From 7814dd4e69b6751467c64bf44c9ab37349a3aa1d Mon Sep 17 00:00:00 2001 From: m Date: Wed, 2 Mar 2022 05:26:55 -0600 Subject: [PATCH] add ui scripting --- src/client/component/ui_scripting.cpp | 176 +++++++++++ src/client/component/ui_scripting.hpp | 12 + src/client/game/symbols.hpp | 15 + src/client/game/ui_scripting/execution.cpp | 161 ++++++++++ src/client/game/ui_scripting/execution.hpp | 18 ++ src/client/game/ui_scripting/lua/context.cpp | 205 +++++++++++++ src/client/game/ui_scripting/lua/context.hpp | 36 +++ src/client/game/ui_scripting/lua/engine.cpp | 61 ++++ src/client/game/ui_scripting/lua/engine.hpp | 8 + src/client/game/ui_scripting/lua/error.cpp | 18 ++ src/client/game/ui_scripting/lua/error.hpp | 8 + .../game/ui_scripting/lua/scheduler.cpp | 122 ++++++++ .../game/ui_scripting/lua/scheduler.hpp | 50 ++++ .../ui_scripting/lua/value_conversion.cpp | 144 +++++++++ .../ui_scripting/lua/value_conversion.hpp | 9 + src/client/game/ui_scripting/script_value.cpp | 274 +++++++++++++++++ src/client/game/ui_scripting/script_value.hpp | 56 ++++ src/client/game/ui_scripting/types.cpp | 276 ++++++++++++++++++ src/client/game/ui_scripting/types.hpp | 89 ++++++ 19 files changed, 1738 insertions(+) create mode 100644 src/client/component/ui_scripting.cpp create mode 100644 src/client/component/ui_scripting.hpp create mode 100644 src/client/game/ui_scripting/execution.cpp create mode 100644 src/client/game/ui_scripting/execution.hpp create mode 100644 src/client/game/ui_scripting/lua/context.cpp create mode 100644 src/client/game/ui_scripting/lua/context.hpp create mode 100644 src/client/game/ui_scripting/lua/engine.cpp create mode 100644 src/client/game/ui_scripting/lua/engine.hpp create mode 100644 src/client/game/ui_scripting/lua/error.cpp create mode 100644 src/client/game/ui_scripting/lua/error.hpp create mode 100644 src/client/game/ui_scripting/lua/scheduler.cpp create mode 100644 src/client/game/ui_scripting/lua/scheduler.hpp create mode 100644 src/client/game/ui_scripting/lua/value_conversion.cpp create mode 100644 src/client/game/ui_scripting/lua/value_conversion.hpp create mode 100644 src/client/game/ui_scripting/script_value.cpp create mode 100644 src/client/game/ui_scripting/script_value.hpp create mode 100644 src/client/game/ui_scripting/types.cpp create mode 100644 src/client/game/ui_scripting/types.hpp diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp new file mode 100644 index 00000000..feb13ffb --- /dev/null +++ b/src/client/component/ui_scripting.cpp @@ -0,0 +1,176 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "scheduler.hpp" +#include "command.hpp" + +#include "ui_scripting.hpp" + +#include "game/ui_scripting/lua/engine.hpp" +#include "game/ui_scripting/execution.hpp" +#include "game/ui_scripting/lua/error.hpp" + +#include +#include + +namespace ui_scripting +{ + namespace + { + std::unordered_map converted_functions; + + utils::hook::detour hksi_lual_error_hook; + utils::hook::detour hksi_lual_error_hook2; + utils::hook::detour hks_start_hook; + utils::hook::detour hks_shutdown_hook; + utils::hook::detour hks_allocator_hook; + utils::hook::detour hks_frame_hook; + + bool error_hook_enabled = false; + + void hksi_lual_error_stub(game::hks::lua_State* s, const char* fmt, ...) + { + char va_buffer[2048] = {0}; + + va_list ap; + va_start(ap, fmt); + vsprintf_s(va_buffer, fmt, ap); + va_end(ap); + + const auto formatted = std::string(va_buffer); + + if (!error_hook_enabled) + { + return hksi_lual_error_hook.invoke(s, formatted.data()); + } + else + { + throw std::runtime_error(formatted); + } + } + + void* hks_start_stub(char a1) + { + const auto _1 = gsl::finally([]() + { + ui_scripting::lua::engine::start(); + }); + + return hks_start_hook.invoke(a1); + } + + void hks_shutdown_stub() + { + ui_scripting::lua::engine::stop(); + hks_shutdown_hook.invoke(); + } + + void* hks_allocator_stub(void* userData, void* oldMemory, unsigned __int64 oldSize, unsigned __int64 newSize) + { + const auto closure = reinterpret_cast(oldMemory); + if (converted_functions.find(closure) != converted_functions.end()) + { + converted_functions.erase(closure); + } + + 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) + { + const auto value = state->m_apistack.base[-1]; + if (value.t != game::hks::TCFUNCTION) + { + return 0; + } + + const auto closure = reinterpret_cast(value.v.cClosure); + if (converted_functions.find(closure) == converted_functions.end()) + { + return 0; + } + + 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(); + + std::vector converted_args; + + for (const auto& argument : arguments) + { + converted_args.push_back(lua::convert(s, argument)); + } + + const auto results = function(sol::as_args(converted_args)); + lua::handle_error(results); + + for (const auto& result : results) + { + push_value(lua::convert({s, result})); + } + + return results.return_count(); + } + + void add_converted_function(game::hks::cclosure* closure, const sol::protected_function& function) + { + converted_functions[closure] = function; + } + + void clear_converted_functions() + { + converted_functions.clear(); + } + + void enable_error_hook() + { + error_hook_enabled = true; + } + + void disable_error_hook() + { + error_hook_enabled = false; + } + + class component final : public component_interface + { + public: + + void post_unpack() override + { + if (!game::environment::is_mp()) + { + return; + } + + hks_start_hook.create(0x140176A40, hks_start_stub); + hks_shutdown_hook.create(0x14016CA80, hks_shutdown_stub); + hksi_lual_error_hook.create(0x14011D4CC, hksi_lual_error_stub); + hksi_lual_error_hook2.create(0x1401366B0, hksi_lual_error_stub); + hks_allocator_hook.create(0x14012BB90, hks_allocator_stub); + hks_frame_hook.create(0x1401755B0, hks_frame_stub); + + command::add("lui_restart", []() + { + utils::hook::invoke(0x14016CA95); + utils::hook::invoke(0x1401780D0); + }); + } + }; +} + +REGISTER_COMPONENT(ui_scripting::component) \ No newline at end of file diff --git a/src/client/component/ui_scripting.hpp b/src/client/component/ui_scripting.hpp new file mode 100644 index 00000000..2a48f6ec --- /dev/null +++ b/src/client/component/ui_scripting.hpp @@ -0,0 +1,12 @@ +#pragma once +#include "game/ui_scripting/lua/value_conversion.hpp" + +namespace ui_scripting +{ + int main_function_handler(game::hks::lua_State* state); + void add_converted_function(game::hks::cclosure* closure, const sol::protected_function& function); + void clear_converted_functions(); + + void enable_error_hook(); + void disable_error_hook(); +} \ No newline at end of file diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 62995ea4..5b832bb1 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -183,4 +183,19 @@ namespace game { WEAK symbol g_entities{0x14550DD90, 0}; } + + namespace hks + { + WEAK symbol lua_state{0, 0x1426D3D08}; + WEAK symbol hksi_lua_pushlstring{0, 0x1400624F0}; + WEAK symbol hks_obj_getfield{0, 0x14012C600}; + WEAK symbol hks_obj_settable{0, 0x14012D820}; + WEAK symbol hks_obj_gettable{0, 0x14012CAE0}; + WEAK symbol vm_call_internal{0, 0x140159EB0}; + WEAK symbol Hashtable_Create{0, 0x14011B320}; + WEAK symbol cclosure_Create{0, 0x14011B540}; + WEAK symbol hksi_luaL_ref{0, 0x140136D30}; + WEAK symbol hksi_luaL_unref{0, 0x14012F610}; + } } diff --git a/src/client/game/ui_scripting/execution.cpp b/src/client/game/ui_scripting/execution.cpp new file mode 100644 index 00000000..fff6b88e --- /dev/null +++ b/src/client/game/ui_scripting/execution.cpp @@ -0,0 +1,161 @@ +#include +#include "execution.hpp" +#include "component/ui_scripting.hpp" + +#include + +namespace ui_scripting +{ + void push_value(const script_value& value) + { + const auto state = *game::hks::lua_state; + const auto value_ = value.get_raw(); + *state->m_apistack.top = value_; + state->m_apistack.top++; + } + + script_value get_return_value(int offset) + { + const auto state = *game::hks::lua_state; + return state->m_apistack.top[-1 - offset]; + } + + arguments get_return_values(int count) + { + arguments values; + + for (auto i = count - 1; i >= 0; i--) + { + values.push_back(get_return_value(i)); + } + + if (values.size() == 0) + { + values.push_back({}); + } + + return values; + } + + arguments call_script_function(const function& function, const arguments& arguments) + { + const auto state = *game::hks::lua_state; + state->m_apistack.top = state->m_apistack.base; + + push_value(function); + for (auto i = arguments.begin(); i != arguments.end(); ++i) + { + push_value(*i); + } + + const auto _1 = gsl::finally(&disable_error_hook); + enable_error_hook(); + + try + { + game::hks::vm_call_internal(state, static_cast(arguments.size()), -1, 0); + const auto count = static_cast(state->m_apistack.top - state->m_apistack.base); + return get_return_values(count); + } + catch (const std::exception& e) + { + throw std::runtime_error(std::string("Error executing script function: ") + e.what()); + } + } + + script_value get_field(const userdata& self, const script_value& key) + { + const auto state = *game::hks::lua_state; + state->m_apistack.top = state->m_apistack.base; + + push_value(key); + + const auto _1 = gsl::finally(&disable_error_hook); + enable_error_hook(); + + game::hks::HksObject value{}; + game::hks::HksObject userdata{}; + userdata.t = game::hks::TUSERDATA; + userdata.v.ptr = self.ptr; + + try + { + game::hks::hks_obj_gettable(&value, state, &userdata, &state->m_apistack.top[-1]); + return value; + } + catch (const std::exception& e) + { + throw std::runtime_error(std::string("Error getting userdata field: ") + e.what()); + } + } + + script_value get_field(const table& self, const script_value& key) + { + const auto state = *game::hks::lua_state; + state->m_apistack.top = state->m_apistack.base; + + push_value(key); + + const auto _1 = gsl::finally(&disable_error_hook); + enable_error_hook(); + + game::hks::HksObject value{}; + game::hks::HksObject userdata{}; + userdata.t = game::hks::TTABLE; + userdata.v.ptr = self.ptr; + + try + { + game::hks::hks_obj_gettable(&value, state, &userdata, &state->m_apistack.top[-1]); + return value; + } + catch (const std::exception& e) + { + throw std::runtime_error(std::string("Error getting table field: ") + e.what()); + } + } + + void set_field(const userdata& self, const script_value& key, const script_value& value) + { + const auto state = *game::hks::lua_state; + state->m_apistack.top = state->m_apistack.base; + + const auto _1 = gsl::finally(&disable_error_hook); + enable_error_hook(); + + game::hks::HksObject userdata{}; + userdata.t = game::hks::TUSERDATA; + userdata.v.ptr = self.ptr; + + try + { + game::hks::hks_obj_settable(state, &userdata, &key.get_raw(), &value.get_raw()); + } + catch (const std::exception& e) + { + throw std::runtime_error(std::string("Error setting userdata field: ") + e.what()); + } + } + + void set_field(const table& self, const script_value& key, const script_value& value) + { + const auto state = *game::hks::lua_state; + state->m_apistack.top = state->m_apistack.base; + + const auto _1 = gsl::finally(&disable_error_hook); + enable_error_hook(); + + game::hks::HksObject userdata{}; + userdata.t = game::hks::TTABLE; + userdata.v.ptr = self.ptr; + + try + { + game::hks::hks_obj_settable(state, &userdata, &key.get_raw(), &value.get_raw()); + } + catch (const std::exception& e) + { + throw std::runtime_error(std::string("Error setting table field: ") + e.what()); + } + } +} diff --git a/src/client/game/ui_scripting/execution.hpp b/src/client/game/ui_scripting/execution.hpp new file mode 100644 index 00000000..24f4dd72 --- /dev/null +++ b/src/client/game/ui_scripting/execution.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "game/game.hpp" +#include "types.hpp" +#include "script_value.hpp" + +namespace ui_scripting +{ + void push_value(const script_value& value); + script_value get_return_value(int offset); + arguments get_return_values(int count); + + arguments call_script_function(const function& function, const arguments& arguments); + + script_value get_field(const userdata& self, const script_value& key); + script_value get_field(const table& self, const script_value& key); + void set_field(const userdata& self, const script_value& key, const script_value& value); + void set_field(const table& self, const script_value& key, const script_value& value); +} 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..d6d05f1d --- /dev/null +++ b/src/client/game/ui_scripting/lua/context.cpp @@ -0,0 +1,205 @@ +#include +#include "context.hpp" +#include "error.hpp" +#include "value_conversion.hpp" +#include "../script_value.hpp" +#include "../execution.hpp" + +#include "../../../component/ui_scripting.hpp" +#include "../../../component/command.hpp" + +#include "component/game_console.hpp" +#include "component/scheduler.hpp" + +#include +#include +#include + +namespace ui_scripting::lua +{ + namespace + { + void setup_types(sol::state& state, scheduler& scheduler) + { + struct game + { + }; + auto game_type = state.new_usertype("game_"); + state["game"] = game(); + + 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); + }; + + auto userdata_type = state.new_usertype("userdata_"); + + userdata_type["new"] = sol::property( + [](const userdata& userdata, const sol::this_state s) + { + return convert(s, userdata.get("new")); + }, + [](const userdata& userdata, const sol::this_state s, const sol::lua_value& value) + { + userdata.set("new", convert({s, value})); + } + ); + + + userdata_type["get"] = [](const userdata& userdata, const sol::this_state s, + const sol::lua_value& key) + { + return convert(s, userdata.get(convert({s, key}))); + }; + + userdata_type["set"] = [](const userdata& userdata, const sol::this_state s, + const sol::lua_value& key, const sol::lua_value& value) + { + userdata.set(convert({s, key}), convert({s, value})); + }; + + userdata_type[sol::meta_function::index] = [](const userdata& userdata, const sol::this_state s, + const sol::lua_value& key) + { + return convert(s, userdata.get(convert({s, key}))); + }; + + userdata_type[sol::meta_function::new_index] = [](const userdata& userdata, const sol::this_state s, + const sol::lua_value& key, const sol::lua_value& value) + { + userdata.set(convert({s, key }), convert({s, value})); + }; + + auto table_type = state.new_usertype("table_"); + + table_type["new"] = sol::property( + [](const table& table, const sol::this_state s) + { + return convert(s, table.get("new")); + }, + [](const table& table, const sol::this_state s, const sol::lua_value& value) + { + table.set("new", convert({s, value})); + } + ); + + table_type["get"] = [](const table& table, const sol::this_state s, + const sol::lua_value& key) + { + return convert(s, table.get(convert({s, key}))); + }; + + table_type["set"] = [](const table& table, const sol::this_state s, + const sol::lua_value& key, const sol::lua_value& value) + { + table.set(convert({s, key}), convert({s, value})); + }; + + table_type[sol::meta_function::index] = [](const table& table, const sol::this_state s, + const sol::lua_value& key) + { + return convert(s, table.get(convert({s, key}))); + }; + + table_type[sol::meta_function::new_index] = [](const table& table, const sol::this_state s, + const sol::lua_value& key, const sol::lua_value& value) + { + table.set(convert({s, key}), convert({s, value})); + }; + + auto function_type = state.new_usertype("function_"); + + function_type[sol::meta_function::call] = [](const function& function, const sol::this_state s, sol::variadic_args va) + { + arguments arguments{}; + + for (auto arg : va) + { + arguments.push_back(convert({s, arg})); + } + + const auto values = function.call(arguments); + std::vector returns; + + for (const auto& value : values) + { + returns.push_back(convert(s, value)); + } + + 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"]; + } + } + + context::context(std::string folder) + : folder_(std::move(folder)) + , scheduler_(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->scheduler_); + + printf("Loading ui script '%s'\n", this->folder_.data()); + this->load_script("__init__"); + } + + context::~context() + { + this->state_.collect_garbage(); + this->scheduler_.clear(); + this->state_ = {}; + } + + void context::run_frame() + { + this->scheduler_.run_frame(); + this->state_.collect_garbage(); + } + + 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..0876d3df --- /dev/null +++ b/src/client/game/ui_scripting/lua/context.hpp @@ -0,0 +1,36 @@ +#pragma once +#pragma warning(push) +#pragma warning(disable: 4702) + +#define SOL_ALL_SAFETIES_ON 1 +#define SOL_PRINT_ERRORS 0 +#include + +#include "scheduler.hpp" + +namespace ui_scripting::lua +{ + 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(); + + private: + sol::state state_{}; + std::string folder_; + std::unordered_set loaded_scripts_; + + scheduler scheduler_; + + 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..7e6d7f6c --- /dev/null +++ b/src/client/game/ui_scripting/lua/engine.cpp @@ -0,0 +1,61 @@ +#include +#include "engine.hpp" +#include "context.hpp" + +#include "../../../component/ui_scripting.hpp" +#include "../../../component/game_module.hpp" + +#include + +namespace ui_scripting::lua::engine +{ + namespace + { + auto& get_scripts() + { + static std::vector> scripts{}; + return scripts; + } + + void load_scripts(const std::string& script_dir) + { + 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 start() + { + clear_converted_functions(); + get_scripts().clear(); + load_scripts(game_module::get_host_module().get_folder() + "/data/ui_scripts/"); + load_scripts("h1-mod/ui_scripts/"); + load_scripts("data/ui_scripts/"); + } + + void stop() + { + clear_converted_functions(); + get_scripts().clear(); + } + + void run_frame() + { + 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..bbcf427c --- /dev/null +++ b/src/client/game/ui_scripting/lua/engine.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace ui_scripting::lua::engine +{ + void start(); + void stop(); + 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..d13b4896 --- /dev/null +++ b/src/client/game/ui_scripting/lua/error.cpp @@ -0,0 +1,18 @@ +#include +#include "error.hpp" + +namespace ui_scripting::lua +{ + void handle_error(const sol::protected_function_result& result) + { + if (!result.valid()) + { + printf("************** UI Script execution error **************\n"); + + const sol::error err = result; + printf("%s\n", err.what()); + + printf("****************************************************\n"); + } + } +} 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/scheduler.cpp b/src/client/game/ui_scripting/lua/scheduler.cpp new file mode 100644 index 00000000..18e779b6 --- /dev/null +++ b/src/client/game/ui_scripting/lua/scheduler.cpp @@ -0,0 +1,122 @@ +#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); + }; + } + + 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::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..1935e25e --- /dev/null +++ b/src/client/game/ui_scripting/lua/scheduler.hpp @@ -0,0 +1,50 @@ +#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; + }; + + 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 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 remove(const task_handle& handle); + void merge_callbacks(); + }; +} diff --git a/src/client/game/ui_scripting/lua/value_conversion.cpp b/src/client/game/ui_scripting/lua/value_conversion.cpp new file mode 100644 index 00000000..38376cdf --- /dev/null +++ b/src/client/game/ui_scripting/lua/value_conversion.cpp @@ -0,0 +1,144 @@ +#include +#include "value_conversion.hpp" +#include "../execution.hpp" +#include "../../../component/ui_scripting.hpp" + +namespace ui_scripting::lua +{ + namespace + { + table convert_table(const sol::table& t) + { + table res{}; + + t.for_each([res](const sol::object& key, const sol::object& value) + { + res.set(convert(key), convert(value)); + }); + + return res; + } + + script_value convert_function(const sol::protected_function& function) + { + const auto closure = game::hks::cclosure_Create(*game::hks::lua_state, main_function_handler, 0, 0, 0); + add_converted_function(closure, function); + + game::hks::HksObject value{}; + value.t = game::hks::TCFUNCTION; + value.v.cClosure = closure; + + return value; + } + } + + 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()}; + } + + if (value.is()) + { + return {value.as()}; + } + + if (value.is
()) + { + return {value.as
()}; + } + + if (value.is()) + { + return {value.as()}; + } + + if (value.is()) + { + return {convert_table(value.as())}; + } + + if (value.is()) + { + return {convert_function(value.as())}; + } + + return {}; + } + + sol::lua_value convert(lua_State* state, const script_value& value) + { + if (value.is()) + { + return {state, value.as()}; + } + + if (value.is()) + { + return {state, value.as()}; + } + + if (value.is()) + { + return {state, value.as()}; + } + + if (value.is()) + { + return {state, value.as()}; + } + + if (value.is()) + { + return {state, value.as()}; + } + + if (value.is()) + { + return {state, value.as()}; + } + + if (value.is
()) + { + return {state, value.as
()}; + } + + if (value.is()) + { + return {state, value.as()}; + } + + return {state, sol::lua_nil}; + } +} diff --git a/src/client/game/ui_scripting/lua/value_conversion.hpp b/src/client/game/ui_scripting/lua/value_conversion.hpp new file mode 100644 index 00000000..21a67e33 --- /dev/null +++ b/src/client/game/ui_scripting/lua/value_conversion.hpp @@ -0,0 +1,9 @@ +#pragma once +#include "context.hpp" +#include "../script_value.hpp" + +namespace ui_scripting::lua +{ + script_value convert(const sol::lua_value& value); + sol::lua_value convert(lua_State* state, const script_value& value); +} diff --git a/src/client/game/ui_scripting/script_value.cpp b/src/client/game/ui_scripting/script_value.cpp new file mode 100644 index 00000000..34d17020 --- /dev/null +++ b/src/client/game/ui_scripting/script_value.cpp @@ -0,0 +1,274 @@ +#include +#include "execution.hpp" +#include "types.hpp" +#include "script_value.hpp" + +namespace ui_scripting +{ + /*************************************************************** + * Constructors + **************************************************************/ + + script_value::script_value(const game::hks::HksObject& value) + : value_(value) + { + } + + script_value::script_value(const int value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TNUMBER; + obj.v.number = static_cast(value); + + this->value_ = obj; + } + + script_value::script_value(const unsigned int value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TNUMBER; + obj.v.number = static_cast(value); + + this->value_ = obj; + } + + script_value::script_value(const bool value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TBOOLEAN; + obj.v.boolean = value; + + this->value_ = obj; + } + + script_value::script_value(const float value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TNUMBER; + obj.v.number = static_cast(value); + + this->value_ = obj; + } + + script_value::script_value(const double value) + : script_value(static_cast(value)) + { + } + + script_value::script_value(const char* value) + { + game::hks::HksObject obj{}; + + const auto state = *game::hks::lua_state; + state->m_apistack.top = state->m_apistack.base; + + game::hks::hksi_lua_pushlstring(state, value, (unsigned int)strlen(value)); + obj = state->m_apistack.top[-1]; + + this->value_ = obj; + } + + script_value::script_value(const std::string& value) + : script_value(value.data()) + { + } + + script_value::script_value(const lightuserdata& value) + { + this->value_.t = game::hks::TLIGHTUSERDATA; + this->value_.v.ptr = value.ptr; + } + + script_value::script_value(const userdata& value) + { + this->value_.t = game::hks::TUSERDATA; + this->value_.v.ptr = value.ptr; + } + + script_value::script_value(const table& value) + { + this->value_.t = game::hks::TTABLE; + this->value_.v.ptr = value.ptr; + } + + script_value::script_value(const function& value) + { + this->value_.t = value.type; + this->value_.v.ptr = value.ptr; + } + + /*************************************************************** + * Integer + **************************************************************/ + + template <> + bool script_value::is() const + { + const auto number = this->get_raw().v.number; + return this->get_raw().t == game::hks::TNUMBER && static_cast(number) == number; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + int script_value::get() const + { + return static_cast(this->get_raw().v.number); + } + + template <> + unsigned int script_value::get() const + { + return static_cast(this->get_raw().v.number); + } + + /*************************************************************** + * Boolean + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TBOOLEAN; + } + + template <> + bool script_value::get() const + { + return this->get_raw().v.boolean; + } + + /*************************************************************** + * Float + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TNUMBER; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + float script_value::get() const + { + return this->get_raw().v.number; + } + + template <> + double script_value::get() const + { + return static_cast(this->get_raw().v.number); + } + + /*************************************************************** + * String + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TSTRING; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + const char* script_value::get() const + { + return this->get_raw().v.str->m_data; + } + + template <> + std::string script_value::get() const + { + return this->get(); + } + + /*************************************************************** + * Lightuserdata + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TLIGHTUSERDATA; + } + + template <> + lightuserdata script_value::get() const + { + return this->get_raw().v.ptr; + } + + /*************************************************************** + * Userdata + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TUSERDATA; + } + + template <> + userdata script_value::get() const + { + return this->get_raw().v.ptr; + } + + /*************************************************************** + * Table + **************************************************************/ + + template <> + bool script_value::is
() const + { + return this->get_raw().t == game::hks::TTABLE; + } + + template <> + table script_value::get() const + { + return this->get_raw().v.table; + } + + /*************************************************************** + * Function + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TIFUNCTION + || this->get_raw().t == game::hks::TCFUNCTION; + } + + template <> + function script_value::get() const + { + return { this->get_raw().v.cClosure, this->get_raw().t }; + } + + /*************************************************************** + * + **************************************************************/ + + const game::hks::HksObject& script_value::get_raw() const + { + return this->value_; + } +} diff --git a/src/client/game/ui_scripting/script_value.hpp b/src/client/game/ui_scripting/script_value.hpp new file mode 100644 index 00000000..3de52ddf --- /dev/null +++ b/src/client/game/ui_scripting/script_value.hpp @@ -0,0 +1,56 @@ +#pragma once +#include "game/game.hpp" + +namespace ui_scripting +{ + class lightuserdata; + class userdata; + class table; + class function; + + class script_value + { + public: + script_value() = default; + script_value(const game::hks::HksObject& value); + + script_value(int value); + script_value(unsigned int value); + script_value(bool value); + + script_value(float value); + script_value(double value); + + script_value(const char* value); + script_value(const std::string& value); + + script_value(const lightuserdata& value); + script_value(const userdata& value); + script_value(const table& value); + script_value(const function& value); + + template + bool is() const; + + template + T as() const + { + if (!this->is()) + { + throw std::runtime_error("Invalid type"); + } + + return get(); + } + + const game::hks::HksObject& get_raw() const; + + private: + template + T get() const; + + game::hks::HksObject value_{}; + }; + + using arguments = std::vector; +} diff --git a/src/client/game/ui_scripting/types.cpp b/src/client/game/ui_scripting/types.cpp new file mode 100644 index 00000000..37032b1b --- /dev/null +++ b/src/client/game/ui_scripting/types.cpp @@ -0,0 +1,276 @@ +#include +#include "types.hpp" +#include "execution.hpp" + +namespace ui_scripting +{ + /*************************************************************** + * Lightuserdata + **************************************************************/ + + lightuserdata::lightuserdata(void* ptr_) + : ptr(ptr_) + { + } + + /*************************************************************** + * Userdata + **************************************************************/ + + userdata::userdata(void* ptr_) + : ptr(ptr_) + { + this->add(); + } + + userdata::userdata(const userdata& other) + { + this->operator=(other); + } + + userdata::userdata(userdata&& other) noexcept + { + this->ptr = other.ptr; + this->ref = other.ref; + other.ref = 0; + } + + userdata::~userdata() + { + this->release(); + } + + userdata& userdata::operator=(const userdata& other) + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->ref = other.ref; + this->add(); + } + + return *this; + } + + userdata& userdata::operator=(userdata&& other) noexcept + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->ref = other.ref; + other.ref = 0; + } + + return *this; + } + + void userdata::add() + { + game::hks::HksObject value{}; + value.v.ptr = this->ptr; + value.t = game::hks::TUSERDATA; + + const auto state = *game::hks::lua_state; + state->m_apistack.top = state->m_apistack.base; + + push_value(value); + + this->ref = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + } + + void userdata::release() + { + if (this->ref) + { + game::hks::hksi_luaL_unref(*game::hks::lua_state, -10000, this->ref); + } + } + + void userdata::set(const script_value& key, const script_value& value) const + { + set_field(*this, key, value); + } + + script_value userdata::get(const script_value& key) const + { + return get_field(*this, key); + } + + /*************************************************************** + * Table + **************************************************************/ + + table::table() + { + const auto state = *game::hks::lua_state; + this->ptr = game::hks::Hashtable_Create(state, 0, 0); + this->add(); + } + + table::table(game::hks::HashTable* ptr_) + : ptr(ptr_) + { + this->add(); + } + + table::table(const table& other) + { + this->operator=(other); + } + + table::table(table&& other) noexcept + { + this->ptr = other.ptr; + this->ref = other.ref; + other.ref = 0; + } + + table::~table() + { + this->release(); + } + + table& table::operator=(const table& other) + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->ref = other.ref; + this->add(); + } + + return *this; + } + + table& table::operator=(table&& other) noexcept + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->ref = other.ref; + other.ref = 0; + } + + return *this; + } + + void table::add() + { + game::hks::HksObject value{}; + value.v.table = this->ptr; + value.t = game::hks::TTABLE; + + const auto state = *game::hks::lua_state; + state->m_apistack.top = state->m_apistack.base; + + push_value(value); + + this->ref = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + } + + void table::release() + { + if (this->ref) + { + game::hks::hksi_luaL_unref(*game::hks::lua_state, -10000, this->ref); + } + } + + void table::set(const script_value& key, const script_value& value) const + { + set_field(*this, key, value); + } + + script_value table::get(const script_value& key) const + { + return get_field(*this, key); + } + + /*************************************************************** + * Function + **************************************************************/ + + function::function(game::hks::cclosure* ptr_, game::hks::HksObjectType type_) + : ptr(ptr_) + , type(type_) + { + this->add(); + } + + function::function(const function& other) + { + this->operator=(other); + } + + function::function(function&& other) noexcept + { + this->ptr = other.ptr; + this->type = other.type; + this->ref = other.ref; + other.ref = 0; + } + + function::~function() + { + this->release(); + } + + function& function::operator=(const function& other) + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->type = other.type; + this->ref = other.ref; + this->add(); + } + + return *this; + } + + function& function::operator=(function&& other) noexcept + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->type = other.type; + this->ref = other.ref; + other.ref = 0; + } + + return *this; + } + + void function::add() + { + game::hks::HksObject value{}; + value.v.cClosure = this->ptr; + value.t = this->type; + + const auto state = *game::hks::lua_state; + state->m_apistack.top = state->m_apistack.base; + + push_value(value); + + this->ref = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + } + + void function::release() + { + if (this->ref) + { + game::hks::hksi_luaL_unref(*game::hks::lua_state, -10000, this->ref); + } + } + + arguments function::call(const arguments& arguments) const + { + return call_script_function(*this, arguments); + } +} diff --git a/src/client/game/ui_scripting/types.hpp b/src/client/game/ui_scripting/types.hpp new file mode 100644 index 00000000..1924407f --- /dev/null +++ b/src/client/game/ui_scripting/types.hpp @@ -0,0 +1,89 @@ +#pragma once +#include "game/game.hpp" +#include "script_value.hpp" + +namespace ui_scripting +{ + class lightuserdata + { + public: + lightuserdata(void*); + void* ptr; + }; + + class userdata + { + public: + userdata(void*); + + userdata(const userdata& other); + userdata(userdata&& other) noexcept; + + ~userdata(); + + userdata& operator=(const userdata& other); + userdata& operator=(userdata&& other) noexcept; + + script_value get(const script_value& key) const; + void set(const script_value& key, const script_value& value) const; + + void* ptr; + + private: + void add(); + void release(); + + int ref{}; + }; + + class table + { + public: + table(); + table(game::hks::HashTable* ptr_); + + table(const table& other); + table(table&& other) noexcept; + + ~table(); + + table& operator=(const table& other); + table& operator=(table&& other) noexcept; + + script_value get(const script_value& key) const; + void set(const script_value& key, const script_value& value) const; + + game::hks::HashTable* ptr; + + private: + void add(); + void release(); + + int ref{}; + }; + + class function + { + public: + function(game::hks::cclosure*, game::hks::HksObjectType); + + function(const function& other); + function(function&& other) noexcept; + + ~function(); + + function& operator=(const function& other); + function& operator=(function&& other) noexcept; + + arguments call(const arguments& arguments) const; + + game::hks::cclosure* ptr; + game::hks::HksObjectType type; + + private: + void add(); + void release(); + + int ref{}; + }; +}