From 885908ff15008d66dc736bdc1eff8e3b1549c4d2 Mon Sep 17 00:00:00 2001 From: m Date: Thu, 7 Mar 2024 20:27:06 -0600 Subject: [PATCH] LUI (#126) * lui scripting + server list * better hooks * add hks hook + fix dvars * add properties util for later * Add Heatbeats to Dedis (#109) * Add heatbeats to dedis * Address review changes * fix: server browser columns match game (#122) * fix: server browser columns match game * chore: remove unused debug logs --------- Co-authored-by: efinst0rm Co-authored-by: Liam --- .gitmodules | 9 + deps/curl | 1 + deps/lua | 1 + deps/premake/curl.lua | 73 +++ deps/premake/l_u_a.lua | 36 ++ deps/premake/sol2.lua | 20 + deps/sol2 | 1 + premake5.lua | 8 +- src/client/component/dedicated.cpp | 50 +- src/client/component/filesystem.cpp | 2 + src/client/component/lui.cpp | 7 + src/client/component/party.cpp | 7 +- src/client/component/scheduler.cpp | 12 + src/client/component/scheduler.hpp | 3 + src/client/component/scripting.cpp | 21 +- src/client/component/scripting.hpp | 4 + src/client/component/server_list.cpp | 525 ++++++++++++++++++ src/client/component/server_list.hpp | 15 + src/client/component/ui_scripting.cpp | 465 ++++++++++++++++ src/client/component/ui_scripting.hpp | 51 ++ src/client/game/structs.hpp | 424 ++++++++++++++ src/client/game/symbols.hpp | 35 +- src/client/game/ui_scripting/execution.cpp | 174 ++++++ src/client/game/ui_scripting/execution.hpp | 23 + src/client/game/ui_scripting/script_value.cpp | 449 +++++++++++++++ src/client/game/ui_scripting/script_value.hpp | 259 +++++++++ src/client/game/ui_scripting/types.cpp | 355 ++++++++++++ src/client/game/ui_scripting/types.hpp | 140 +++++ src/client/resource.hpp | 7 + src/common/utils/http.cpp | 122 +++- src/common/utils/http.hpp | 18 +- src/common/utils/properties.cpp | 24 + src/common/utils/properties.hpp | 6 + 33 files changed, 3302 insertions(+), 45 deletions(-) create mode 160000 deps/curl create mode 160000 deps/lua create mode 100644 deps/premake/curl.lua create mode 100644 deps/premake/l_u_a.lua create mode 100644 deps/premake/sol2.lua create mode 160000 deps/sol2 create mode 100644 src/client/component/server_list.cpp create mode 100644 src/client/component/server_list.hpp 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/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 create mode 100644 src/common/utils/properties.cpp create mode 100644 src/common/utils/properties.hpp diff --git a/.gitmodules b/.gitmodules index 24839421..c19efef0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -35,3 +35,12 @@ path = deps/gsc-tool url = https://github.com/Joelrau/gsc-tool.git branch = iw7 +[submodule "deps/sol2"] + path = deps/sol2 + url = https://github.com/ThePhD/sol2.git +[submodule "deps/lua"] + path = deps/lua + url = https://github.com/lua/lua.git +[submodule "deps/curl"] + path = deps/curl + url = https://github.com/curl/curl.git diff --git a/deps/curl b/deps/curl new file mode 160000 index 00000000..3f08d80b --- /dev/null +++ b/deps/curl @@ -0,0 +1 @@ +Subproject commit 3f08d80b2244524646ce86915c585509ac54fb4c diff --git a/deps/lua b/deps/lua new file mode 160000 index 00000000..7923dbbf --- /dev/null +++ b/deps/lua @@ -0,0 +1 @@ +Subproject commit 7923dbbf72da303ca1cca17efd24725668992f15 diff --git a/deps/premake/curl.lua b/deps/premake/curl.lua new file mode 100644 index 00000000..8db164e1 --- /dev/null +++ b/deps/premake/curl.lua @@ -0,0 +1,73 @@ +curl = { + source = path.join(dependencies.basePath, "curl"), +} + +function curl.import() + links { "curl" } + + filter "toolset:msc*" + links { "Crypt32.lib" } + filter {} + + curl.includes() +end + +function curl.includes() +filter "toolset:msc*" + includedirs { + path.join(curl.source, "include"), + } + + defines { + "CURL_STRICTER", + "CURL_STATICLIB", + "CURL_DISABLE_LDAP", + } +filter {} +end + +function curl.project() + if not os.istarget("windows") then + return + end + + project "curl" + language "C" + + curl.includes() + + includedirs { + path.join(curl.source, "lib"), + } + + files { + path.join(curl.source, "lib/**.c"), + path.join(curl.source, "lib/**.h"), + } + + defines { + "BUILDING_LIBCURL", + } + + filter "toolset:msc*" + + defines { + "USE_SCHANNEL", + "USE_WINDOWS_SSPI", + "USE_THREADS_WIN32", + } + + filter "toolset:not msc*" + + defines { + "USE_GNUTLS", + "USE_THREADS_POSIX", + } + + filter {} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, curl) \ No newline at end of file diff --git a/deps/premake/l_u_a.lua b/deps/premake/l_u_a.lua new file mode 100644 index 00000000..c2b139fc --- /dev/null +++ b/deps/premake/l_u_a.lua @@ -0,0 +1,36 @@ +l_u_a = { + source = path.join(dependencies.basePath, "lua"), +} + +function l_u_a.import() + links { "lua" } + l_u_a.includes() +end + +function l_u_a.includes() + includedirs { + l_u_a.source + } + +end + +function l_u_a.project() + project "lua" + language "C" + + l_u_a.includes() + + files { + path.join(l_u_a.source, "*.h"), + path.join(l_u_a.source, "*.c"), + } + + removefiles { + path.join(l_u_a.source, "onelua.c"), + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, l_u_a) \ No newline at end of file diff --git a/deps/premake/sol2.lua b/deps/premake/sol2.lua new file mode 100644 index 00000000..3f495f48 --- /dev/null +++ b/deps/premake/sol2.lua @@ -0,0 +1,20 @@ +sol2 = { + source = path.join(dependencies.basePath, "sol2"), +} + +function sol2.import() + sol2.includes() + l_u_a.import() +end + +function sol2.includes() + includedirs { + path.join(sol2.source, "include") + } +end + +function sol2.project() + +end + +table.insert(dependencies, sol2) \ No newline at end of file diff --git a/deps/sol2 b/deps/sol2 new file mode 160000 index 00000000..e8e122e9 --- /dev/null +++ b/deps/sol2 @@ -0,0 +1 @@ +Subproject commit e8e122e9ce46f4f1c0b04003d8b703fe1b89755a diff --git a/premake5.lua b/premake5.lua index de48ee9c..cd4971c9 100644 --- a/premake5.lua +++ b/premake5.lua @@ -331,13 +331,19 @@ links {"common"} prebuildcommands {"pushd %{_MAIN_SCRIPT_DIR}", "tools\\premake5 generate-buildinfo", "popd"} -COMPUTER_NAME = os.getenv('COMPUTERNAME') +local COMPUTER_NAME = os.getenv('COMPUTERNAME') if COMPUTER_NAME == "JOEL-PC" then debugdir "D:\\Games\\PC\\IW7" debugcommand "D:\\Games\\PC\\IW7\\$(TargetName)$(TargetExt)" postbuildcommands { "copy /y \"$(OutDir)$(TargetName)$(TargetExt)\" \"D:\\Games\\PC\\IW7\\$(TargetName)$(TargetExt)\"", } +elseif COMPUTER_NAME == "DESKTOP-P7PCR6I" or COMPUTER_NAME == "mikey" then + debugdir "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Call of Duty - Infinite Warfare" + debugcommand "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Call of Duty - Infinite Warfare\\$(TargetName)$(TargetExt)" + postbuildcommands { + "copy /y \"$(OutDir)$(TargetName)$(TargetExt)\" \"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Call of Duty - Infinite Warfare\\$(TargetName)$(TargetExt)\"", + } end if _OPTIONS["copy-to"] then diff --git a/src/client/component/dedicated.cpp b/src/client/component/dedicated.cpp index 3941d2c6..ce849e85 100644 --- a/src/client/component/dedicated.cpp +++ b/src/client/component/dedicated.cpp @@ -9,6 +9,8 @@ #include "console/console.hpp" #include "scheduler.hpp" #include "filesystem.hpp" +#include "server_list.hpp" +#include "network.hpp" #include @@ -23,6 +25,8 @@ namespace dedicated { utils::hook::detour com_quit_f_hook; + const game::dvar_t* sv_lanOnly; + void kill_server() { game::SV_MainMP_KillLocalServer(); @@ -77,6 +81,39 @@ namespace dedicated game::Scr_AddInt(0); } + void send_heartbeat() + { + if (sv_lanOnly->current.enabled) + { + return; + } + + game::netadr_s target{}; + if (server_list::get_master_server(target)) + { + network::send(target, "heartbeat", "IW7"); + } + } + + void sys_error_stub(const char* msg, ...) + { + char buffer[2048]{}; + + va_list ap; + va_start(ap, msg); + + vsnprintf_s(buffer, _TRUNCATE, msg, ap); + + va_end(ap); + + scheduler::once([] + { + command::execute("map_rotate"); + }, scheduler::main, 3s); + + game::Com_Error(game::ERR_DROP, "%s", buffer); + } + void init_dedicated_server() { // R_RegisterDvars @@ -189,7 +226,7 @@ namespace dedicated game::Dvar_RegisterBool("dedicated", true, game::DVAR_FLAG_READ, "Dedicated server"); // Add lanonly mode - game::Dvar_RegisterBool("sv_lanOnly", false, game::DVAR_FLAG_NONE, "Don't send heartbeat"); + sv_lanOnly = game::Dvar_RegisterBool("sv_lanOnly", false, game::DVAR_FLAG_NONE, "Don't send heartbeat"); // Disable frontend //dvars::override::register_bool("frontEndSceneEnabled", false, game::DVAR_FLAG_READ); @@ -203,6 +240,9 @@ namespace dedicated dvars::override::register_bool("intro", false, game::DVAR_FLAG_READ); + // Stop crashing from sys_errors + utils::hook::jump(0xD34180_b, sys_error_stub, true); + // Is party dedicated utils::hook::jump(0x5DFC10_b, party_is_server_dedicated_stub); @@ -340,6 +380,12 @@ namespace dedicated game::Cmd_RemoveCommand("disconnect"); execute_startup_command_queue(); + + // Send heartbeat to dpmaster + scheduler::once(send_heartbeat, scheduler::pipeline::server); + scheduler::loop(send_heartbeat, scheduler::pipeline::server, 10min); + command::add("heartbeat", send_heartbeat); + }, scheduler::pipeline::main, 1s); // dedicated info @@ -378,4 +424,4 @@ namespace dedicated }; } -REGISTER_COMPONENT(dedicated::component) \ No newline at end of file +REGISTER_COMPONENT(dedicated::component) diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp index 2d427119..9a5e51b0 100644 --- a/src/client/component/filesystem.cpp +++ b/src/client/component/filesystem.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace filesystem { @@ -51,6 +52,7 @@ namespace filesystem initialized = true; + filesystem::register_path(utils::properties::get_appdata_path() / "cdata"); // CLIENT_DATA_FOLDER filesystem::register_path(L"."); filesystem::register_path(L"iw7-mod"); filesystem::register_path(L"devraw_shared"); diff --git a/src/client/component/lui.cpp b/src/client/component/lui.cpp index 855885f9..b0926fd9 100644 --- a/src/client/component/lui.cpp +++ b/src/client/component/lui.cpp @@ -111,6 +111,13 @@ namespace lui game::LUI_CoD_Shutdown(); game::LUI_CoD_Init(game::Com_FrontEnd_IsInFrontEnd(), false); }); + + command::add("runMenuScript", [](const command::params& params) + { + const auto args_str = params.join(1); + const auto* args = args_str.data(); + game::UI_RunMenuScript(0, &args); + }); } }; } diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index f03ba720..22aff3a5 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -12,6 +12,7 @@ #include "network.hpp" #include "profile_infos.hpp" #include "scheduler.hpp" +#include "server_list.hpp" #include #include @@ -330,7 +331,7 @@ namespace party game::Com_SetLocalizedErrorMessage(error.data(), "MENU_NOTICE"); } - connection_state get_server_connection_state() + connection_state get_server_connection_state() { return server_connection_state; } @@ -504,8 +505,8 @@ namespace party network::on("infoResponse", [](const game::netadr_s& target, const std::string_view& data) { - const utils::info_string info = data; - //server_list::handle_info_response(target, info); + const utils::info_string info{ data }; + server_list::handle_info_response(target, info); if (server_connection_state.host != target) { diff --git a/src/client/component/scheduler.cpp b/src/client/component/scheduler.cpp index f9fee12a..53f9f55d 100644 --- a/src/client/component/scheduler.cpp +++ b/src/client/component/scheduler.cpp @@ -89,6 +89,7 @@ namespace scheduler utils::hook::detour r_end_frame_hook; utils::hook::detour g_run_frame_hook; utils::hook::detour main_frame_hook; + utils::hook::detour hks_frame_hook; void execute(const pipeline type) { @@ -117,6 +118,16 @@ namespace scheduler return main_frame_hook.invoke(); } + + void hks_frame_stub() + { + const auto state = *game::hks::lua_state; + if (state) + { + execute(pipeline::lui); + } + hks_frame_hook.invoke(); + } } void schedule(const std::function& callback, const pipeline type, @@ -188,6 +199,7 @@ namespace scheduler r_end_frame_hook.create(0xE267B0_b, scheduler::r_end_frame_stub); g_run_frame_hook.create(0xB15E20_b, scheduler::server_frame_stub); main_frame_hook.create(0xB8E2D0_b, scheduler::main_frame_stub); + hks_frame_hook.create(0x613440_b, scheduler::hks_frame_stub); } void pre_destroy() override diff --git a/src/client/component/scheduler.hpp b/src/client/component/scheduler.hpp index f26c60ce..68f78d54 100644 --- a/src/client/component/scheduler.hpp +++ b/src/client/component/scheduler.hpp @@ -16,6 +16,9 @@ namespace scheduler // The game's main thread main, + // LUI context + lui, + count, }; diff --git a/src/client/component/scripting.cpp b/src/client/component/scripting.cpp index 5fc1a1d4..b78b889a 100644 --- a/src/client/component/scripting.cpp +++ b/src/client/component/scripting.cpp @@ -24,6 +24,8 @@ namespace scripting std::unordered_map>> script_function_table_sort; std::unordered_map> script_function_table_rev; + utils::concurrency::container shared_table; + std::string current_file; namespace @@ -47,19 +49,16 @@ namespace scripting void vm_notify_stub(const unsigned int notify_list_owner_id, const game::scr_string_t string_value, game::VariableValue* top) { - if (!game::Com_FrontEnd_IsInFrontEnd()) + const auto* string = game::SL_ConvertToString(string_value); + if (string) { - const auto* string = game::SL_ConvertToString(string_value); - if (string) - { - event e{}; - e.name = string; - e.entity = notify_list_owner_id; + event e {}; + e.name = string; + e.entity = notify_list_owner_id; - for (auto* value = top; value->type != game::VAR_PRECODEPOS; --value) - { - e.arguments.emplace_back(*value); - } + for (auto* value = top; value->type != game::VAR_PRECODEPOS; --value) + { + e.arguments.emplace_back(*value); } } diff --git a/src/client/component/scripting.hpp b/src/client/component/scripting.hpp index 46048217..12598b9c 100644 --- a/src/client/component/scripting.hpp +++ b/src/client/component/scripting.hpp @@ -3,11 +3,15 @@ namespace scripting { + using shared_table_t = std::unordered_map; + extern std::unordered_map> fields_table; extern std::unordered_map> script_function_table; extern std::unordered_map>> script_function_table_sort; extern std::unordered_map> script_function_table_rev; + extern utils::concurrency::container shared_table; + extern std::string current_file; void on_shutdown(const std::function& callback); diff --git a/src/client/component/server_list.cpp b/src/client/component/server_list.cpp new file mode 100644 index 00000000..e4883705 --- /dev/null +++ b/src/client/component/server_list.cpp @@ -0,0 +1,525 @@ +#include +#include "loader/component_loader.hpp" + +#include "command.hpp" +#include "console/console.hpp" +#include "fastfiles.hpp" +#include "localized_strings.hpp" +#include "network.hpp" +#include "party.hpp" +#include "scheduler.hpp" +#include "server_list.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" +#include "game/ui_scripting/execution.hpp" + +#include +#include +#include + +namespace server_list +{ + namespace + { + const int server_limit = 100; + + struct server_info + { + int clients; + int max_clients; + int bots; + int ping; + std::string host_name; + std::string map_name; + std::string game_type; + std::string mod_name; + game::GameModeType play_mode; + char in_game; + game::netadr_s address; + bool is_private; + }; + + struct + { + game::netadr_s address{}; + volatile bool requesting = false; + std::unordered_map queued_servers{}; + } master_state; + + std::mutex mutex; + std::vector servers; + + size_t server_list_page = 0; + volatile bool update_server_list = false; + std::chrono::high_resolution_clock::time_point last_scroll{}; + + game::dvar_t* master_server_ip; + game::dvar_t* master_server_port; + + size_t get_page_count() + { + const auto count = servers.size() / server_limit; + return count + (servers.size() % server_limit > 0); + } + + size_t get_page_base_index() + { + return server_list_page * server_limit; + } + + void refresh_server_list() + { + { + std::lock_guard _(mutex); + servers.clear(); + master_state.queued_servers.clear(); + server_list_page = 0; + } + + party::reset_server_connection_state(); + + if (get_master_server(master_state.address)) + { + master_state.requesting = true; + network::send(master_state.address, "getservers", utils::string::va("IW7 %i full empty", PROTOCOL)); + } + } + + void join_server(int, int, const int index) + { + std::lock_guard _(mutex); + + const auto i = static_cast(index) + get_page_base_index(); + if (i < servers.size()) + { + static auto last_index = ~0ull; + if (last_index != i) + { + last_index = i; + } + else + { + party::connect(servers[i].address); + } + } + } + + void trigger_refresh() + { + update_server_list = true; + } + + + const char* ui_feeder_item_text(int arg0, int arg1, const int index, const int column, char* name) + { + std::lock_guard _(mutex); + + const auto i = get_page_base_index() + index; + if (i >= servers.size()) + { + return ""; + } + + switch (column) + { + case 2: + return servers[i].host_name.empty() ? "" : servers[i].host_name.data(); + case 3: + { + const auto& map_name = servers[i].map_name; + if (map_name.empty()) + { + return "Unknown"; + } + + auto map_display_name = game::UI_GetMapDisplayName(map_name.data()); + if (!fastfiles::exists(map_name)) // TODO: add "false" 2nd parameter if usermaps come + { + map_display_name = utils::string::va("^1%s", map_display_name); + } + return map_display_name; + } + case 4: + { + const auto client_count = servers[i].clients - servers[i].bots; + return utils::string::va("%d/%d [%d]", client_count, servers[i].max_clients, + servers[i].clients); + } + case 5: + return servers[i].game_type.empty() ? "" : servers[i].game_type.data(); + //case 10: + //{ + // const auto ping = servers[i].ping ? servers[i].ping : 999; + // if (ping < 75) + // { + // return utils::string::va("^2%d", ping); + // } + // else if (ping < 150) + // { + // return utils::string::va("^3%d", ping); + // } + // return utils::string::va("^1%d", ping); + //} + case 10: + return servers[i].in_game ? "0" : "1"; + //case 6: + // return servers[i].mod_name.empty() ? "" : servers[i].mod_name.data(); + default: + return ""; + } + } + + void sort_serverlist() + { + std::stable_sort(servers.begin(), servers.end(), [](const server_info& a, const server_info& b) + { + const auto a_players = a.clients - a.bots; + const auto b_players = b.clients - b.bots; + + if (a_players == b_players) + { + if (a.clients == b.clients) + { + return a.ping < b.ping; + } + + return a.clients > b.clients; + } + + return a_players > b_players; + }); + } + + void insert_server(server_info&& server) + { + std::lock_guard _(mutex); + servers.emplace_back(std::move(server)); + sort_serverlist(); + trigger_refresh(); + } + + void do_frame_work() + { + auto& queue = master_state.queued_servers; + if (queue.empty()) + { + return; + } + + std::lock_guard _(mutex); + + size_t queried_servers = 0; + const size_t query_limit = 3; + + for (auto i = queue.begin(); i != queue.end();) + { + if (i->second) + { + const auto now = game::Sys_Milliseconds(); + if (now - i->second > 10'000) + { + i = queue.erase(i); + continue; + } + } + else if (queried_servers++ < query_limit) + { + i->second = game::Sys_Milliseconds(); + network::send(i->first, "getInfo", utils::cryptography::random::get_challenge()); + } + + ++i; + } + } + + bool is_server_list_open() + { +#ifdef DEBUG + return false; +#else + // TODO + return game::Menu_IsMenuOpenAndVisible(0, "menu_systemlink_join"); +#endif + } + + bool is_scrolling_disabled() + { + return update_server_list || (std::chrono::high_resolution_clock::now() - last_scroll) < 500ms; + } + + bool scroll_down() + { + if (!is_server_list_open()) + { + return false; + } + + if (!is_scrolling_disabled() && server_list_page + 1 < get_page_count()) + { + last_scroll = std::chrono::high_resolution_clock::now(); + ++server_list_page; + trigger_refresh(); + } + + return true; + } + + bool scroll_up() + { + if (!is_server_list_open()) + { + return false; + } + + if (!is_scrolling_disabled() && server_list_page > 0) + { + last_scroll = std::chrono::high_resolution_clock::now(); + --server_list_page; + trigger_refresh(); + } + + return true; + } + + utils::hook::detour lui_open_menu_hook; + + void lui_open_menu_stub(int controllerIndex, const char* menuName, int isPopup, int isModal, unsigned int isExclusive) + { + if (!strcmp(menuName, "SystemLinkMenu")) + { + refresh_server_list(); + } + + lui_open_menu_hook.invoke(controllerIndex, menuName, isPopup, isModal, isExclusive); + } + + void check_refresh() + { + if (update_server_list) + { + update_server_list = false; + ui_scripting::notify("updateGameList", {}); + } + } + } + + bool sl_key_event(const int key, const int down) + { + if (down) + { + if (key == game::keyNum_t::K_MWHEELUP) + { + return !scroll_up(); + } + + if (key == game::keyNum_t::K_MWHEELDOWN) + { + return !scroll_down(); + } + } + + return true; + } + + bool get_master_server(game::netadr_s& address) + { + return game::NET_StringToAdr(utils::string::va("%s:%s", + master_server_ip->current.string, master_server_port->current.string), &address); + } + + void handle_info_response(const game::netadr_s& address, const utils::info_string& info) + { + // Don't show servers that aren't using the same protocol! + const auto protocol = std::atoi(info.get("protocol").data()); + if (protocol != PROTOCOL) + { + return; + } + + // Don't show servers that aren't dedicated! + const auto dedicated = std::atoi(info.get("dedicated").data()); + if (!dedicated) + { + return; + } + + // Don't show servers that aren't running! + const auto sv_running = std::atoi(info.get("sv_running").data()); + if (!sv_running) + { + return; + } + + // Only handle servers of the same playmode! + const auto playmode = game::GameModeType(std::atoi(info.get("playmode").data())); + if (game::Com_GameMode_GetActiveGameMode() != playmode) + { + return; + } + + if (info.get("gamename") != "IW7") + { + return; + } + + int start_time{}; + const auto now = game::Sys_Milliseconds(); + + { + std::lock_guard _(mutex); + const auto entry = master_state.queued_servers.find(address); + + if (entry == master_state.queued_servers.end() || !entry->second) + { + return; + } + + start_time = entry->second; + master_state.queued_servers.erase(entry); + } + + server_info server{}; + server.address = address; + server.host_name = info.get("hostname"); + server.map_name = info.get("mapname"); // UI_GetMapDisplayName? + server.game_type = game::UI_GetGameTypeDisplayName(info.get("gametype").data()); + server.mod_name = info.get("fs_game"); + server.play_mode = playmode; + server.clients = atoi(info.get("clients").data()); + server.max_clients = atoi(info.get("sv_maxclients").data()); + server.bots = atoi(info.get("bots").data()); + server.ping = std::min(now - start_time, 999); + server.is_private = atoi(info.get("isPrivate").data()) == 1; + + server.in_game = 1; + +#ifdef DEBUG + console::debug("inserting server \"%s\"\n", server.host_name.data()); +#endif + + insert_server(std::move(server)); + } + + int get_player_count() + { + std::lock_guard _(mutex); + auto count = 0; + for (const auto& server : servers) + { + count += server.clients - server.bots; + } + return count; + } + + int get_server_count() + { + std::lock_guard _(mutex); + return static_cast(servers.size()); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + scheduler::once([]() + { + // add dvars to change destination master server ip/port + master_server_ip = game::Dvar_RegisterString("masterServerIP", "server.alterware.dev", game::DVAR_FLAG_NONE, + "IP of the destination master server to connect to"); + master_server_port = game::Dvar_RegisterString("masterServerPort", "20810", game::DVAR_FLAG_NONE, + "Port of the destination master server to connect to"); + }, scheduler::pipeline::main); + + if (game::environment::is_dedi()) + { + return; + } + + // hook LUI_OpenMenu to refresh server list for system link menu + lui_open_menu_hook.create(game::LUI_OpenMenu, lui_open_menu_stub); + + // replace UI_RunMenuScript call in LUI_CoD_LuaCall_RefreshServerList to our refresh_servers + utils::hook::jump(0x69E019_b, utils::hook::assemble([](utils::hook::assembler& a) + { + a.pushad64(); + a.call_aligned(refresh_server_list); + a.popad64(); + + a.xor_(eax, eax); + a.mov(rbx, qword_ptr(rsp, 0x38)); + a.add(rsp, 0x20); + a.pop(rdi); + a.ret(); + }), true); + + utils::hook::jump(0x69E9F7_b, utils::hook::assemble([](utils::hook::assembler& a) + { + a.mov(r8d, edi); + a.mov(ecx, eax); + a.mov(ebx, eax); + + a.pushad64(); + a.call_aligned(join_server); + a.popad64(); + + a.jmp(0x69EA03_b); + }), true); + + utils::hook::nop(0x69EA1D_b, 5); + + utils::hook::call(0x69E45E_b, get_server_count); + utils::hook::jump(0xCC5F00_b, ui_feeder_item_text); + + scheduler::loop(do_frame_work, scheduler::pipeline::main); + scheduler::loop(check_refresh, scheduler::pipeline::lui, 10ms); + + network::on("getServersResponse", [](const game::netadr_s& target, const std::string_view& data) + { + { + std::lock_guard _(mutex); + if (!master_state.requesting || master_state.address != target) + { + return; + } + + master_state.requesting = false; + + std::optional start{}; + for (std::size_t i = 0; i + 6 < data.size(); ++i) + { + if (data[i + 6] == '\\') + { + start.emplace(i); + break; + } + } + + if (!start.has_value()) + { + return; + } + + for (auto i = start.value(); i + 6 < data.size(); i += 7) + { + if (data[i + 6] != '\\') + { + break; + } + + game::netadr_s address{}; + address.type = game::NA_IP; + address.localNetID = game::NS_CLIENT1; + std::memcpy(&address.ip[0], data.data() + i + 0, 4); + std::memcpy(&address.port, data.data() + i + 4, 2); + + master_state.queued_servers[address] = 0; + } + } + }); + } + }; +} + +REGISTER_COMPONENT(server_list::component) diff --git a/src/client/component/server_list.hpp b/src/client/component/server_list.hpp new file mode 100644 index 00000000..0a86a0c4 --- /dev/null +++ b/src/client/component/server_list.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "game/game.hpp" +#include + +namespace server_list +{ + bool get_master_server(game::netadr_s& address); + void handle_info_response(const game::netadr_s& address, const utils::info_string& info); + + bool sl_key_event(int key, int down); + + int get_player_count(); + int get_server_count(); +} diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp new file mode 100644 index 00000000..f247571d --- /dev/null +++ b/src/client/component/ui_scripting.cpp @@ -0,0 +1,465 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + + +#include "command.hpp" +#include "console/console.hpp" +#include "fastfiles.hpp" +#include "filesystem.hpp" +#include "game_module.hpp" +#include "localized_strings.hpp" +#include "party.hpp" +#include "scheduler.hpp" +#include "scripting.hpp" +#include "server_list.hpp" + +#include "game/ui_scripting/execution.hpp" +//#include "game/scripting/execution.hpp" + +#include "ui_scripting.hpp" + +#include +#include +#include +#include + +#include "steam/steam.hpp" + +namespace ui_scripting +{ + namespace + { + std::unordered_map> converted_functions; + + utils::hook::detour hks_start_hook; + utils::hook::detour hks_shutdown_hook; + utils::hook::detour hks_package_require_hook; + + utils::hook::detour hks_load_hook; + + /* + const auto lui_common = utils::nt::load_resource(LUI_COMMON); + const auto lui_updater = utils::nt::load_resource(LUI_UPDATER); + const auto lua_json = utils::nt::load_resource(LUA_JSON); + */ + + struct globals_t + { + std::string in_require_script; + std::unordered_map loaded_scripts; + bool load_raw_script{}; + std::string raw_script_name{}; + }; + + globals_t globals{}; + + bool is_loaded_script(const std::string& name) + { + return globals.loaded_scripts.contains(name); + } + + std::string get_root_script(const std::string& name) + { + const auto itr = globals.loaded_scripts.find(name); + return itr == globals.loaded_scripts.end() ? std::string() : itr->second; + } + + void print_error(const std::string& error) + { + console::error("************** LUI script execution error **************\n"); + console::error("%s\n", error.data()); + console::error("********************************************************\n"); + } + + void print_loading_script(const std::string& name) + { + console::info("Loading LUI script '%s'\n", name.data()); + } + + std::string get_current_script() + { + const auto state = *game::hks::lua_state; + game::hks::lua_Debug info{}; + game::hks::hksi_lua_getstack(state, 1, &info); + game::hks::hksi_lua_getinfo(state, "nSl", &info); + return info.short_src; + } + + int load_buffer(const std::string& name, const std::string& data) + { + const auto state = *game::hks::lua_state; + const auto sharing_mode = state->m_global->m_bytecodeSharingMode; + state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_ON; + const auto _0 = gsl::finally([&]() + { + state->m_global->m_bytecodeSharingMode = sharing_mode; + }); + + game::hks::HksCompilerSettings compiler_settings{}; + return game::hks::hksi_hksL_loadbuffer(state, &compiler_settings, data.data(), data.size(), name.data()); + } + + void load_script(const std::string& name, const std::string& data) + { + globals.loaded_scripts[name] = name; + + const auto lua = get_globals(); + const auto load_results = lua["loadstring"](data, name); + + if (load_results[0].is()) + { + const auto results = lua["pcall"](load_results); + if (!results[0].as()) + { + print_error(results[1].as()); + } + } + else if (load_results[1].is()) + { + print_error(load_results[1].as()); + } + } + + 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) + { + std::string data{}; + if (std::filesystem::is_directory(script) && utils::io::read_file(script + "/__init__.lua", &data)) + { + print_loading_script(script); + load_script(script + "/__init__.lua", data); + } + } + } + + void setup_functions() + { + const auto lua = get_globals(); + + lua["io"]["fileexists"] = utils::io::file_exists; + lua["io"]["writefile"] = utils::io::write_file; + lua["io"]["movefile"] = utils::io::move_file; + lua["io"]["filesize"] = utils::io::file_size; + lua["io"]["createdirectory"] = utils::io::create_directory; + lua["io"]["directoryexists"] = utils::io::directory_exists; + lua["io"]["directoryisempty"] = utils::io::directory_is_empty; + lua["io"]["listfiles"] = utils::io::list_files; + lua["io"]["removefile"] = utils::io::remove_file; + lua["io"]["readfile"] = static_cast(utils::io::read_file); + + using game = table; + auto game_type = game(); + lua["game"] = game_type; + + /* + game_type["addlocalizedstring"] = [](const game&, const std::string& string, + const std::string& value) + { + localized_strings::override(string, value); + }; + */ + + game_type["sharedset"] = [](const game&, const std::string& key, const std::string& value) + { + scripting::shared_table.access([key, value](scripting::shared_table_t& table) + { + table[key] = value; + }); + }; + + game_type["sharedget"] = [](const game&, const std::string& key) + { + std::string result; + scripting::shared_table.access([key, &result](scripting::shared_table_t& table) + { + result = table[key]; + }); + return result; + }; + + game_type["sharedclear"] = [](const game&) + { + scripting::shared_table.access([](scripting::shared_table_t& table) + { + table.clear(); + }); + }; + + /* + game_type["assetlist"] = [](const game&, const std::string& type_string) + { + auto table_ = table(); + auto index = 1; + auto type_index = -1; + for (auto i = 0; i < ::game::XAssetType::ASSET_TYPE_COUNT; i++) + { + if (type_string == ::game::g_assetNames[i]) + { + type_index = i; + } + } + if (type_index == -1) + { + throw std::runtime_error("Asset type does not exist"); + } + const auto type = static_cast<::game::XAssetType>(type_index); + fastfiles::enum_assets(type, [type, &table_, &index](const ::game::XAssetHeader header) + { + const auto asset = ::game::XAsset{type, header}; + const std::string asset_name = ::game::DB_GetXAssetName(&asset); + table_[index++] = asset_name; + }, true); + return table_; + }; + */ + + game_type["getcurrentgamelanguage"] = [](const game&) + { + return steam::SteamApps()->GetCurrentGameLanguage(); + }; + + game_type["isdefaultmaterial"] = [](const game&, const std::string& material) + { + return static_cast(::game::DB_IsXAssetDefault(::game::ASSET_TYPE_MATERIAL, + material.data())); + }; + + auto server_list_table = table(); + lua["serverlist"] = server_list_table; + + server_list_table["getplayercount"] = server_list::get_player_count; + server_list_table["getservercount"] = server_list::get_server_count; + } + + void enable_globals() + { + const auto lua = get_globals(); + const std::string code = + "local g = getmetatable(_G)\n" + "if not g then\n" + "g = {}\n" + "setmetatable(_G, g)\n" + "end\n" + "g.__newindex = nil\n"; + + lua["loadstring"](code)[0](); + } + + void start() + { + globals = {}; + const auto lua = get_globals(); + enable_globals(); // EnableGlobals() isn't a thing? + + setup_functions(); + + lua["print"] = [](const variadic_args& va) + { + std::string buffer{}; + const auto to_string = get_globals()["tostring"]; + + for (auto i = 0; i < va.size(); i++) + { + const auto& arg = va[i]; + const auto str = to_string(arg)[0].as(); + buffer.append(str); + + if (i < va.size() - 1) + { + buffer.append("\t"); + } + } + + console::info("%s\n", buffer.data()); + }; + + lua["table"]["unpack"] = lua["unpack"]; + lua["luiglobals"] = lua; + + /* + load_script("lui_common", lui_common); + load_script("lui_updater", lui_updater); + load_script("lua_json", lua_json); + */ + + for (const auto& path : filesystem::get_search_paths_rev()) + { + load_scripts(path + "/ui_scripts/"); + } + } + + void try_start() + { + try + { + start(); + } + catch (const std::exception& e) + { + console::error("Failed to load LUI scripts: %s\n", e.what()); + } + } + + void* hks_start_stub(char a1, char a2) + { + const auto _0 = gsl::finally(&try_start); + return hks_start_hook.invoke(a1, a2); + } + + void hks_shutdown_stub() + { + converted_functions.clear(); + globals = {}; + return hks_shutdown_hook.invoke(); + } + + void* hks_package_require_stub(game::hks::lua_State* state) + { + const auto script = get_current_script(); + const auto root = get_root_script(script); + globals.in_require_script = root; + return hks_package_require_hook.invoke(state); + } + + game::XAssetHeader db_find_x_asset_header_stub(game::XAssetType type, const char* name, int allow_create_default) + { + game::XAssetHeader header{.luaFile = nullptr}; + + if (!is_loaded_script(globals.in_require_script)) + { + return game::DB_FindXAssetHeader(type, name, allow_create_default); + } + + const auto folder = globals.in_require_script.substr(0, globals.in_require_script.find_last_of("/\\")); + const std::string name_ = name; + const std::string target_script = folder + "/" + name_ + ".lua"; + + if (utils::io::file_exists(target_script)) + { + globals.load_raw_script = true; + globals.raw_script_name = target_script; + header.luaFile = reinterpret_cast(1); + } + else if (name_.starts_with("ui/")) + { + return game::DB_FindXAssetHeader(type, name, allow_create_default); + } + + return header; + } + + int hks_load_stub(game::hks::lua_State* state, void* compiler_options, + void* reader, void* reader_data, const char* chunk_name) + { + if (globals.load_raw_script) + { + globals.load_raw_script = false; + globals.loaded_scripts[globals.raw_script_name] = globals.in_require_script; + return load_buffer(globals.raw_script_name, utils::io::read_file(globals.raw_script_name)); + } + + return hks_load_hook.invoke(state, compiler_options, reader, + reader_data, chunk_name); + } + + std::string current_error; + int main_handler(game::hks::lua_State* state) + { + bool error = false; + + try + { + const auto value = state->m_apistack.base[-1]; + if (value.t != game::hks::TCFUNCTION) + { + return 0; + } + + const auto closure = value.v.cClosure; + if (!converted_functions.contains(closure)) + { + return 0; + } + + const auto& function = converted_functions[closure]; + + const auto args = get_return_values(); + const auto results = function(args); + + for (const auto& result : results) + { + push_value(result); + } + + return static_cast(results.size()); + } + catch (const std::exception& e) + { + current_error = e.what(); + error = true; + } + + if (error) + { + game::hks::hksi_luaL_error(state, current_error.data()); + } + + return 0; + } + } + + table get_globals() + { + const auto state = *game::hks::lua_state; + return state->globals.v.table; + } + + template + game::hks::cclosure* convert_function(F f) + { + const auto state = *game::hks::lua_state; + const auto closure = game::hks::cclosure_Create(state, main_handler, 0, 0, 0); + converted_functions[closure] = wrap_function(f); + return closure; + } + + bool lui_running() + { + return *game::hks::lua_state != nullptr; + } + + class component final : public component_interface + { + public: + + void post_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + utils::hook::call(0x5FC2F7_b, db_find_x_asset_header_stub); + utils::hook::call(0x5FC0AB_b, db_find_x_asset_header_stub); + + hks_load_hook.create(0x11E0B00_b, hks_load_stub); + + hks_package_require_hook.create(0x11C7F00_b, hks_package_require_stub); + hks_start_hook.create(0x615090_b, hks_start_stub); + hks_shutdown_hook.create(0x6124B0_b, hks_shutdown_stub); + } + }; +} + +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..2d01867b --- /dev/null +++ b/src/client/component/ui_scripting.hpp @@ -0,0 +1,51 @@ +#pragma once + +namespace ui_scripting +{ + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f](const function_arguments& args) + { + f(args[I]...); + return arguments{{}}; + }; + } + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f](const function_arguments& args) + { + return f(args[I]...); + }; + } + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f](const function_arguments& args) + { + return arguments{f(args[I]...)}; + }; + } + + template + auto wrap_function(const std::function& f) + { + return wrap_function(f, std::index_sequence_for{}); + } + + template + auto wrap_function(F f) + { + return wrap_function(std::function(f)); + } + + table get_globals(); + + template + game::hks::cclosure* convert_function(F f); + + bool lui_running(); +} diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 8029b039..9923437b 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -1047,5 +1047,429 @@ namespace game PhysicalMemoryPrim prim[2]; }; } + + namespace hks + { + struct lua_State; + struct HashTable; + struct cclosure; + + struct GenericChunkHeader + { + unsigned __int64 m_flags; + }; + + struct ChunkHeader : GenericChunkHeader + { + ChunkHeader* m_next; + }; + + struct UserData : ChunkHeader + { + unsigned __int64 m_envAndSizeOffsetHighBits; + unsigned __int64 m_metaAndSizeOffsetLowBits; + char m_data[8]; + }; + + struct InternString + { + unsigned __int64 m_flags; + unsigned __int64 m_lengthbits; + unsigned int m_hash; + char m_data[30]; + }; + + union HksValue + { + cclosure* cClosure; + void* closure; + UserData* userData; + HashTable* table; + void* tstruct; + InternString* str; + void* thread; + void* ptr; + float number; + long long i64; + unsigned long long ui64; + unsigned int native; + bool boolean; + }; + + enum HksObjectType + { + TANY = 0xFFFFFFFE, + TNONE = 0xFFFFFFFF, + TNIL = 0x0, + TBOOLEAN = 0x1, + TLIGHTUSERDATA = 0x2, + TNUMBER = 0x3, + TSTRING = 0x4, + TTABLE = 0x5, + TFUNCTION = 0x6, // idk + TUSERDATA = 0x7, + TTHREAD = 0x8, + TIFUNCTION = 0x9, // Lua function + TCFUNCTION = 0xA, // C function + TUI64 = 0xB, + TSTRUCT = 0xC, + NUM_TYPE_OBJECTS = 0xE, + }; + + struct HksObject + { + HksObjectType t; + HksValue v; + }; + + const struct hksInstruction + { + unsigned int code; + }; + + struct ActivationRecord + { + HksObject* m_base; + const hksInstruction* m_returnAddress; + __int16 m_tailCallDepth; + __int16 m_numVarargs; + int m_numExpectedReturns; + }; + + struct CallStack + { + ActivationRecord* m_records; + ActivationRecord* m_lastrecord; + ActivationRecord* m_current; + const hksInstruction* m_current_lua_pc; + const hksInstruction* m_hook_return_addr; + int m_hook_level; + }; + + struct ApiStack + { + HksObject* top; + HksObject* base; + HksObject* alloc_top; + HksObject* bottom; + }; + + struct UpValue : ChunkHeader + { + HksObject m_storage; + HksObject* loc; + UpValue* m_next; + }; + + struct CallSite + { + _SETJMP_FLOAT128 m_jumpBuffer[16]; + CallSite* m_prev; + }; + + enum Status + { + NEW = 0x1, + RUNNING = 0x2, + YIELDED = 0x3, + DEAD_ERROR = 0x4, + }; + + enum HksError + { + HKS_NO_ERROR = 0x0, + HKS_ERRSYNTAX = 0xFFFFFFFC, + HKS_ERRFILE = 0xFFFFFFFB, + HKS_ERRRUN = 0xFFFFFF9C, + HKS_ERRMEM = 0xFFFFFF38, + HKS_ERRERR = 0xFFFFFED4, + HKS_THROWING_ERROR = 0xFFFFFE0C, + HKS_GC_YIELD = 0x1, + }; + + struct lua_Debug + { + int event; + const char* name; + const char* namewhat; + const char* what; + const char* source; + int currentline; + int nups; + int nparams; + int ishksfunc; + int linedefined; + int lastlinedefined; + char short_src[512]; + int callstack_level; + int is_tail_call; + }; + + using lua_function = int(__fastcall*)(lua_State*); + + struct luaL_Reg + { + const char* name; + lua_function function; + }; + + struct Node + { + HksObject m_key; + HksObject m_value; + }; + + struct Metatable + { + }; + + struct HashTable : ChunkHeader + { + Metatable* m_meta; + unsigned int m_version; + unsigned int m_mask; + Node* m_hashPart; + HksObject* m_arrayPart; + unsigned int m_arraySize; + Node* m_freeNode; + }; + + struct cclosure : ChunkHeader + { + lua_function m_function; + HashTable* m_env; + __int16 m_numUpvalues; + __int16 m_flags; + InternString* m_name; + HksObject m_upvalues[1]; + }; + + enum HksCompilerSettings_BytecodeSharingFormat + { + BYTECODE_DEFAULT = 0x0, + BYTECODE_INPLACE = 0x1, + BYTECODE_REFERENCED = 0x2, + }; + + enum HksCompilerSettings_IntLiteralOptions + { + INT_LITERALS_NONE = 0x0, + INT_LITERALS_LUD = 0x1, + INT_LITERALS_32BIT = 0x1, + INT_LITERALS_UI64 = 0x2, + INT_LITERALS_64BIT = 0x2, + INT_LITERALS_ALL = 0x3, + }; + + struct HksCompilerSettings + { + int m_emitStructCode; + const char** m_stripNames; + int m_emitGlobalMemoization; + int _m_isHksGlobalMemoTestingMode; + HksCompilerSettings_BytecodeSharingFormat m_bytecodeSharingFormat; + HksCompilerSettings_IntLiteralOptions m_enableIntLiterals; + int(__fastcall* m_debugMap)(const char*, int); + }; + + enum HksBytecodeSharingMode + { + HKS_BYTECODE_SHARING_OFF = 0x0, + HKS_BYTECODE_SHARING_ON = 0x1, + HKS_BYTECODE_SHARING_SECURE = 0x2, + }; + + struct HksGcWeights + { + int m_removeString; + int m_finalizeUserdataNoMM; + int m_finalizeUserdataGcMM; + int m_cleanCoroutine; + int m_removeWeak; + int m_markObject; + int m_traverseString; + int m_traverseUserdata; + int m_traverseCoroutine; + int m_traverseWeakTable; + int m_freeChunk; + int m_sweepTraverse; + }; + + struct GarbageCollector_Stack + { + void* m_storage; + unsigned int m_numEntries; + unsigned int m_numAllocated; + }; + + struct ProtoList + { + void** m_protoList; + unsigned __int16 m_protoSize; + unsigned __int16 m_protoAllocSize; + }; + + struct GarbageCollector + { + int m_target; + int m_stepsLeft; + int m_stepLimit; + HksGcWeights m_costs; + int m_unit; + _SETJMP_FLOAT128(*m_jumpPoint)[16]; + lua_State* m_mainState; + lua_State* m_finalizerState; + void* m_memory; + int m_phase; + GarbageCollector_Stack m_resumeStack; + GarbageCollector_Stack m_greyStack; + GarbageCollector_Stack m_remarkStack; + GarbageCollector_Stack m_weakStack; + int m_finalizing; + HksObject m_safeTableValue; + lua_State* m_startOfStateStackList; + lua_State* m_endOfStateStackList; + lua_State* m_currentState; + HksObject m_safeValue; + void* m_compiler; + void* m_bytecodeReader; + void* m_bytecodeWriter; + int m_pauseMultiplier; + int m_stepMultiplier; + bool m_stopped; + int(__fastcall* m_gcPolicy)(lua_State*); + unsigned __int64 m_pauseTriggerMemoryUsage; + int m_stepTriggerCountdown; + unsigned int m_stringTableIndex; + unsigned int m_stringTableSize; + UserData* m_lastBlackUD; + UserData* m_activeUD; + }; + + enum MemoryManager_ChunkColor + { + RED = 0x0, + BLACK = 0x1, + }; + + struct ChunkList + { + ChunkHeader m_prevToStart; + }; + + enum Hks_DeleteCheckingMode + { + HKS_DELETE_CHECKING_OFF = 0x0, + HKS_DELETE_CHECKING_ACCURATE = 0x1, + HKS_DELETE_CHECKING_SAFE = 0x2, + }; + + struct MemoryManager + { + void* (__fastcall* m_allocator)(void*, void*, unsigned __int64, unsigned __int64); + void* m_allocatorUd; + MemoryManager_ChunkColor m_chunkColor; + unsigned __int64 m_used; + unsigned __int64 m_highwatermark; + ChunkList m_allocationList; + ChunkList m_sweepList; + ChunkHeader* m_lastKeptChunk; + lua_State* m_state; + ChunkList m_deletedList; + int m_deleteMode; + Hks_DeleteCheckingMode m_deleteCheckingMode; + }; + + struct StaticStringCache + { + HksObject m_objects[41]; + }; + + enum HksBytecodeEndianness + { + HKS_BYTECODE_DEFAULT_ENDIAN = 0x0, + HKS_BYTECODE_BIG_ENDIAN = 0x1, + HKS_BYTECODE_LITTLE_ENDIAN = 0x2, + }; + + struct RuntimeProfileData_Stats + { + unsigned __int64 hksTime; + unsigned __int64 callbackTime; + unsigned __int64 gcTime; + unsigned __int64 cFinalizerTime; + unsigned __int64 compilerTime; + unsigned int hkssTimeSamples; + unsigned int callbackTimeSamples; + unsigned int gcTimeSamples; + unsigned int compilerTimeSamples; + unsigned int num_newuserdata; + unsigned int num_tablerehash; + unsigned int num_pushstring; + unsigned int num_pushcfunction; + unsigned int num_newtables; + }; + + struct RuntimeProfileData + { + __int64 stackDepth; + __int64 callbackDepth; + unsigned __int64 lastTimer; + RuntimeProfileData_Stats frameStats; + unsigned __int64 gcStartTime; + unsigned __int64 finalizerStartTime; + unsigned __int64 compilerStartTime; + unsigned __int64 compilerStartGCTime; + unsigned __int64 compilerStartGCFinalizerTime; + unsigned __int64 compilerCallbackStartTime; + __int64 compilerDepth; + void* outFile; + lua_State* rootState; + }; + + struct HksGlobal + { + MemoryManager m_memory; + GarbageCollector m_collector; + StringTable m_stringTable; + HksBytecodeSharingMode m_bytecodeSharingMode; + unsigned int m_tableVersionInitializer; + HksObject m_registry; + ProtoList m_protoList; + HashTable* m_structProtoByName; + ChunkList m_userDataList; + lua_State* m_root; + StaticStringCache m_staticStringCache; + void* m_debugger; + void* m_profiler; + RuntimeProfileData m_runProfilerData; + HksCompilerSettings m_compilerSettings; + int(__fastcall* m_panicFunction)(lua_State*); + void* m_luaplusObjectList; + int m_heapAssertionFrequency; + int m_heapAssertionCount; + void (*m_logFunction)(lua_State*, const char*, ...); + HksBytecodeEndianness m_bytecodeDumpEndianness; + }; + + struct lua_State : ChunkHeader + { + HksGlobal* m_global; + CallStack m_callStack; + ApiStack m_apistack; + UpValue* pending; + HksObject globals; + HksObject m_cEnv; + CallSite* m_callsites; + int m_numberOfCCalls; + void* m_context; + InternString* m_name; + lua_State* m_nextState; + lua_State* m_nextStateStack; + Status m_status; + HksError m_error; + }; + } + using namespace pmem; } diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 4b57b985..17346310 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -154,6 +154,9 @@ namespace game WEAK symbol Lobby_GetPartyData{ 0x9C3E20 }; + WEAK symbol LUI_EnterCriticalSection{ 0x600080 }; + WEAK symbol LUI_LeaveCriticalSection{ 0x602280 }; + WEAK symbol Material_RegisterHandle{ 0xE11CE0 }; WEAK symbol NetadrToSockadr{ 0xCE6B90 }; @@ -216,7 +219,8 @@ namespace game WEAK symbol Scr_AddInt{ 0xC0A580 }; WEAK symbol Scr_CastString{ 0xC06AE0 }; WEAK symbol Scr_ClearOutParams{ 0xC0ABC0 }; - WEAK symbol Scr_GetEntityIdRef{ 0xC09050 }; + WEAK symbol Scr_GetEntityId{ 0xC08FA0 }; + WEAK symbol Scr_GetEntityIdRef{ 0xC09050 }; WEAK symbol Scr_SetObjectField{ 0x40B6E0 }; WEAK symbol Scr_GetInt{ 0xC0B950 }; WEAK symbol Scr_ErrorInternal{ 0xC0AC30 }; @@ -251,6 +255,10 @@ namespace game WEAK symbol SND_StopSounds{ 0xCA06E0 }; WEAK symbol SND_SetMusicState{ 0xC9E110 }; + WEAK symbol UI_GetMapDisplayName{ 0xCC6270 }; + WEAK symbol UI_GetGameTypeDisplayName{ 0xCC61C0 }; + WEAK symbol UI_RunMenuScript{ 0xCC9710 }; + WEAK symbol longjmp{ 0x12C0758 }; WEAK symbol _setjmp{ 0x1423110 }; @@ -318,4 +326,29 @@ namespace game WEAK symbol g_streamPos{ 0x5687E30 }; WEAK symbol g_quitRequested{ 0x779CD44 }; + + WEAK symbol gameEntityId{ 0x665A124 }; + WEAK symbol level_time{ 0x3C986D8 }; + + namespace hks + { + WEAK symbol lua_state{ 0x4FC35F0 }; + WEAK symbol hksi_lua_pushlstring{ 0x309E0 }; + WEAK symbol hks_obj_getfield{ 0x11E14D0 }; + WEAK symbol hks_obj_gettable{ 0x11E19B0 }; + WEAK symbol hks_obj_settable{ 0x11E26F0 }; + WEAK symbol vm_call_internal{ 0x120CCE0 }; + WEAK symbol Hashtable_Create{ 0x11D0590 }; + WEAK symbol cclosure_Create{ 0x11D07B0 }; + WEAK symbol hksi_luaL_ref{ 0x11EAE10 }; + WEAK symbol hksi_hksL_loadbuffer{ 0x11E2F50 }; + WEAK symbol hksi_lua_getinfo{ 0x11E48C0 }; + WEAK symbol hksi_lua_getstack{ 0x11E4AA0 }; + WEAK symbol hksi_luaL_error{ 0x11EA860 }; + WEAK symbol hksi_lua_gc{ 0x11EAF00 }; + WEAK symbol hksi_luaL_unref{ 0x11E4460 }; + WEAK symbol s_compilerTypeName{ 0x1BDEEF0 }; + } } diff --git a/src/client/game/ui_scripting/execution.cpp b/src/client/game/ui_scripting/execution.cpp new file mode 100644 index 00000000..1961e041 --- /dev/null +++ b/src/client/game/ui_scripting/execution.cpp @@ -0,0 +1,174 @@ +#include +#include "execution.hpp" +#include "component/ui_scripting.hpp" +#include "component/console/console.hpp" + +#include + +namespace ui_scripting +{ + namespace + { + script_value get_field(void* ptr, game::hks::HksObjectType type, const script_value& key) + { + const auto state = *game::hks::lua_state; + const auto top = state->m_apistack.top; + + push_value(key); + + game::hks::HksObject value{}; + game::hks::HksObject obj{}; + obj.t = type; + obj.v.ptr = ptr; + + game::hks::hks_obj_gettable(&value, state, &obj, &state->m_apistack.top[-1]); + state->m_apistack.top = top; + return value; + } + + void set_field(void* ptr, game::hks::HksObjectType type, const script_value& key, const script_value& value) + { + const auto state = *game::hks::lua_state; + + game::hks::HksObject obj{}; + obj.t = type; + obj.v.ptr = ptr; + + game::hks::hks_obj_settable(state, &obj, &key.get_raw(), &value.get_raw()); + } + } + + void push_value(const script_value& value) + { + const auto state = *game::hks::lua_state; + *state->m_apistack.top = value.get_raw(); + state->m_apistack.top++; + } + + void push_value(const game::hks::HksObject& value) + { + const auto state = *game::hks::lua_state; + *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() + { + const auto state = *game::hks::lua_state; + const auto count = static_cast(state->m_apistack.top - state->m_apistack.base); + arguments values; + + for (auto i = count - 1; i >= 0; i--) + { + const auto v = get_return_value(i); + values.push_back(v); + } + + if (values.empty()) + { + values.push_back({}); + } + + return values; + } + + arguments get_return_values(game::hks::HksObject* base) + { + const auto state = *game::hks::lua_state; + const auto count = static_cast(state->m_apistack.top - base); + arguments values; + + for (auto i = count - 1; i >= 0; i--) + { + const auto v = get_return_value(i); + values.push_back(v); + } + + if (values.empty()) + { + values.push_back({}); + } + + return values; + } + + bool notify(const std::string& name, const event_arguments& arguments) + { + const auto state = *game::hks::lua_state; + if (state == nullptr) + { + return false; + } + + const auto _1 = gsl::finally(game::LUI_LeaveCriticalSection); + game::LUI_EnterCriticalSection(); + + try + { + const auto engine = get_globals().get("Engine").as(); + const auto root = engine.get("GetLuiRoot")()[0].as(); + const auto process_event = root.get("processEvent"); + + table event{}; + event.set("name", name); + event.set("dispatchChildren", true); + + for (const auto& arg : arguments) + { + event.set(arg.first, arg.second); + } + + process_event(root, event); + return true; + } + catch (const std::exception& e) + { + console::error("Error processing event '%s' %s\n", name.data(), e.what()); + } + + return false; + } + + arguments call_script_function(const function& function, const arguments& arguments) + { + const auto state = *game::hks::lua_state; + const auto top = state->m_apistack.top; + + push_value(function); + for (auto i = arguments.begin(); i != arguments.end(); ++i) + { + push_value(*i); + } + + game::hks::vm_call_internal(state, static_cast(arguments.size()), -1, 0); + const auto args = get_return_values(top); + state->m_apistack.top = top; + return args; + } + + script_value get_field(const userdata& self, const script_value& key) + { + return get_field(self.ptr, game::hks::TUSERDATA, key); + } + + script_value get_field(const table& self, const script_value& key) + { + return get_field(self.ptr, game::hks::TTABLE, key); + } + + void set_field(const userdata& self, const script_value& key, const script_value& value) + { + set_field(self.ptr, game::hks::TUSERDATA, key, value); + } + + void set_field(const table& self, const script_value& key, const script_value& value) + { + set_field(self.ptr, game::hks::TTABLE, key, value); + } +} diff --git a/src/client/game/ui_scripting/execution.hpp b/src/client/game/ui_scripting/execution.hpp new file mode 100644 index 00000000..bc39e7a0 --- /dev/null +++ b/src/client/game/ui_scripting/execution.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "game/game.hpp" +#include "types.hpp" +#include "script_value.hpp" + +namespace ui_scripting +{ + void push_value(const script_value& value); + void push_value(const game::hks::HksObject& value); + + script_value get_return_value(int offset); + arguments get_return_values(); + arguments get_return_values(game::hks::HksObject* base); + + bool notify(const std::string& name, const event_arguments& arguments); + + 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/script_value.cpp b/src/client/game/ui_scripting/script_value.cpp new file mode 100644 index 00000000..5f728f75 --- /dev/null +++ b/src/client/game/ui_scripting/script_value.cpp @@ -0,0 +1,449 @@ +#include +#include "execution.hpp" +#include "types.hpp" +#include "script_value.hpp" +#include "../../component/ui_scripting.hpp" + +namespace ui_scripting +{ + hks_object::hks_object(const game::hks::HksObject& value) + { + this->assign(value); + } + + hks_object::hks_object(const hks_object& other) noexcept + { + this->operator=(other); + } + + hks_object::hks_object(hks_object&& other) noexcept + { + this->operator=(std::move(other)); + } + + hks_object& hks_object::operator=(const hks_object& other) noexcept + { + if (this != &other) + { + this->release(); + this->assign(other.value_); + } + + return *this; + } + + hks_object& hks_object::operator=(hks_object&& other) noexcept + { + if (this != &other) + { + this->release(); + this->value_ = other.value_; + other.value_.t = game::hks::TNONE; + } + + return *this; + } + + hks_object::~hks_object() + { + this->release(); + } + + const game::hks::HksObject& hks_object::get() const + { + return this->value_; + } + + void hks_object::assign(const game::hks::HksObject& value) + { + this->value_ = value; + + const auto state = *game::hks::lua_state; + const auto top = state->m_apistack.top; + + push_value(this->value_); + this->ref_ = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + state->m_apistack.top = top; + } + + void hks_object::release() + { + if (this->ref_) + { + game::hks::hksi_luaL_unref(*game::hks::lua_state, -10000, this->ref_); + this->value_.t = game::hks::TNONE; + } + } + + /*************************************************************** + * 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 long long value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TUI64; + obj.v.i64 = value; + + this->value_ = obj; + } + + script_value::script_value(const unsigned long long value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TUI64; + obj.v.ui64 = 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, const size_t len) + { + game::hks::HksObject obj{}; + + const auto state = *game::hks::lua_state; + if (state == nullptr) + { + return; + } + + const auto top = state->m_apistack.top; + game::hks::hksi_lua_pushlstring(state, value, static_cast(len)); + obj = state->m_apistack.top[-1]; + state->m_apistack.top = top; + + this->value_ = obj; + } + + script_value::script_value(const char* value) + : script_value(value, strlen(value)) + { + } + + script_value::script_value(const std::string& value) + : script_value(value.data(), value.size()) + { + } + + script_value::script_value(const lightuserdata& value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TLIGHTUSERDATA; + obj.v.ptr = value.ptr; + + this->value_ = obj; + } + + script_value::script_value(const userdata& value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TUSERDATA; + obj.v.ptr = value.ptr; + + this->value_ = obj; + } + + script_value::script_value(const table& value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TTABLE; + obj.v.ptr = value.ptr; + + this->value_ = obj; + } + + script_value::script_value(const function& value) + { + game::hks::HksObject obj{}; + obj.t = value.type; + obj.v.ptr = value.ptr; + + this->value_ = obj; + } + + /*************************************************************** + * 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); + } + + /*************************************************************** + * Integer 64 + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TUI64; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + long long script_value::get() const + { + return static_cast(this->get_raw().v.ui64); + } + + template <> + unsigned long long script_value::get() const + { + return static_cast(this->get_raw().v.ui64); + } + + /*************************************************************** + * 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_.get(); + } + + bool script_value::operator==(const script_value& other) const + { + if (this->get_raw().t != other.get_raw().t) + { + return false; + } + + if (this->get_raw().t == game::hks::TSTRING) + { + return this->get() == other.get(); + } + + return this->get_raw().v.native == other.get_raw().v.native; + } + + arguments script_value::operator()() const + { + return this->as()(); + } + + arguments script_value::operator()(const arguments& arguments) const + { + return this->as()(arguments); + } + + function_argument::function_argument(const arguments& args, const script_value& value, const int index) + : values_(args) + , value_(value) + , index_(index) + { + } + + function_arguments::function_arguments(const arguments& values) + : values_(values) + { + } +} 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..12042c9d --- /dev/null +++ b/src/client/game/ui_scripting/script_value.hpp @@ -0,0 +1,259 @@ +#pragma once +#include "game/game.hpp" + +#include + +namespace ui_scripting +{ + class lightuserdata; + class userdata_value; + class userdata; + class table_value; + class table; + class function; + class script_value; + + namespace + { + template + std::string get_typename() + { + auto& info = typeid(T); + + if (info == typeid(std::string) || + info == typeid(const char*)) + { + return "string"; + } + + if (info == typeid(lightuserdata)) + { + return "lightuserdata"; + } + + if (info == typeid(userdata)) + { + return "userdata"; + } + + if (info == typeid(table)) + { + return "table"; + } + + if (info == typeid(function)) + { + return "function"; + } + + if (info == typeid(int) || + info == typeid(float) || + info == typeid(unsigned int)) + { + return "number"; + } + + if (info == typeid(bool)) + { + return "boolean"; + } + + return info.name(); + } + } + + class hks_object + { + public: + hks_object() = default; + hks_object(const game::hks::HksObject& value); + hks_object(const hks_object& other) noexcept; + hks_object(hks_object&& other) noexcept; + + hks_object& operator=(const hks_object& other) noexcept; + hks_object& operator=(hks_object&& other) noexcept; + + ~hks_object(); + + const game::hks::HksObject& get() const; + + private: + void assign(const game::hks::HksObject& value); + void release(); + + game::hks::HksObject value_{game::hks::TNONE, {}}; + int ref_{}; + }; + + using arguments = std::vector; + using event_arguments = std::unordered_map; + + 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(long long value); + script_value(unsigned long long value); + script_value(bool value); + + script_value(float value); + script_value(double value); + + script_value(const char* value); + script_value(const char* value, const size_t len); + 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 class C, class T, typename TableType = table> + script_value(const C>& container) + { + TableType table_{}; + int index = 1; + + for (const auto& value : container) + { + table_.set(index++, value); + } + + game::hks::HksObject obj{}; + obj.t = game::hks::TTABLE; + obj.v.ptr = table_.ptr; + + this->value_ = obj; + } + + template + script_value(F f) + : script_value(function(f)) + { + } + + bool operator==(const script_value& other) const; + + arguments operator()() const; + arguments operator()(const arguments& arguments) const; + + template + arguments operator()(T... arguments) const + { + return this->as().call({arguments...}); + } + + template + table_value operator[](const char(&key)[Size]) const + { + return {this->as
(), key}; + } + + template + table_value operator[](const T& key) const + { + return {this->as
(), key}; + } + + template + bool is() const; + + template + T as() const + { + if (!this->is()) + { + const auto hks_typename = game::hks::s_compilerTypeName[this->get_raw().t + 2]; + const auto typename_ = get_typename(); + + throw std::runtime_error(utils::string::va("%s expected, got %s", + typename_.data(), hks_typename)); + } + + return get(); + } + + template + operator T() const + { + return this->as(); + } + + const game::hks::HksObject& get_raw() const; + + hks_object value_{}; + + private: + template + T get() const; + + }; + + class variadic_args : public arguments + { + }; + + class function_argument + { + public: + function_argument(const arguments& args, const script_value& value, const int index); + + template + T as() const + { + try + { + return this->value_.as(); + } + catch (const std::exception& e) + { + throw std::runtime_error(utils::string::va("bad argument #%d (%s)", + this->index_ + 1, e.what())); + } + } + + template <> + variadic_args as() const + { + variadic_args args{}; + for (auto i = this->index_; i < this->values_.size(); i++) + { + args.push_back(this->values_[i]); + } + return args; + } + + template + operator T() const + { + return this->as(); + } + + private: + arguments values_{}; + script_value value_{}; + int index_{}; + }; + + class function_arguments + { + public: + function_arguments(const arguments& values); + + function_argument operator[](const int index) const + { + if (index >= values_.size()) + { + return {values_, {}, index}; + } + + return {values_, values_[index], index}; + } + private: + arguments values_{}; + }; +} diff --git a/src/client/game/ui_scripting/types.cpp b/src/client/game/ui_scripting/types.cpp new file mode 100644 index 00000000..125bf380 --- /dev/null +++ b/src/client/game/ui_scripting/types.cpp @@ -0,0 +1,355 @@ +#include +#include "types.hpp" +#include "execution.hpp" +#include "../../component/ui_scripting.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; + const auto top = state->m_apistack.top; + + push_value(value); + + this->ref = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + state->m_apistack.top = top; + } + + 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); + } + + userdata_value userdata::operator[](const script_value& key) const + { + return {*this, key}; + } + + userdata_value::userdata_value(const userdata& table, const script_value& key) + : userdata_(table) + , key_(key) + { + this->value_ = this->userdata_.get(key).get_raw(); + } + + void userdata_value::operator=(const script_value& value) + { + this->userdata_.set(this->key_, value); + this->value_ = value.get_raw(); + } + + bool userdata_value::operator==(const script_value& value) + { + return this->userdata_.get(this->key_) == value; + } + + /*************************************************************** + * 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; + const auto top = state->m_apistack.top; + + push_value(value); + + this->ref = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + state->m_apistack.top = top; + } + + 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); + } + + table_value table::operator[](const script_value& key) const + { + return {*this, key}; + } + + script_value table::get(const script_value& key) const + { + return get_field(*this, key); + } + + table_value::table_value(const table& table, const script_value& key) + : table_(table) + , key_(key) + { + this->value_ = this->table_.get(key).get_raw(); + } + + void table_value::operator=(const script_value& value) + { + this->table_.set(this->key_, value); + this->value_ = value.get_raw(); + } + + void table_value::operator=(const table_value& value) + { + this->table_.set(this->key_, value); + this->value_ = value.get_raw(); + } + + bool table_value::operator==(const script_value& value) + { + return this->table_.get(this->key_) == value; + } + + bool table_value::operator==(const table_value& value) + { + return this->table_.get(this->key_) == value; + } + + /*************************************************************** + * Function + **************************************************************/ + + function::function(game::hks::lua_function func) + { + const auto state = *game::hks::lua_state; + this->ptr = game::hks::cclosure_Create(state, func, 0, 0, 0); + this->type = game::hks::HksObjectType::TCFUNCTION; + this->add(); + } + + 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; + const auto top = state->m_apistack.top; + + push_value(value); + + this->ref = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + state->m_apistack.top = top; + } + + 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); + } + + arguments function::operator()(const arguments& arguments) const + { + return this->call(arguments); + } + + arguments function::operator()() const + { + return this->call({}); + } +} diff --git a/src/client/game/ui_scripting/types.hpp b/src/client/game/ui_scripting/types.hpp new file mode 100644 index 00000000..d5e94b4b --- /dev/null +++ b/src/client/game/ui_scripting/types.hpp @@ -0,0 +1,140 @@ +#pragma once +#include "game/game.hpp" +#include "script_value.hpp" +#include "../../component/ui_scripting.hpp" + +namespace ui_scripting +{ + class lightuserdata + { + public: + lightuserdata(void*); + void* ptr; + }; + + class userdata_value; + + 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; + + userdata_value operator[](const script_value& key) const; + + void* ptr; + + private: + void add(); + void release(); + + int ref{}; + }; + + class userdata_value : public script_value + { + public: + userdata_value(const userdata& table, const script_value& key); + void operator=(const script_value& value); + bool operator==(const script_value& value); + private: + userdata userdata_; + script_value key_; + }; + + class table_value; + + 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; + + table_value operator[](const script_value& key) const; + + game::hks::HashTable* ptr; + + private: + void add(); + void release(); + + int ref{}; + }; + + class table_value : public script_value + { + public: + table_value(const table& table, const script_value& key); + void operator=(const script_value& value); + void operator=(const table_value& value); + bool operator==(const script_value& value); + bool operator==(const table_value& value); + private: + table table_; + script_value key_; + }; + + class function + { + public: + function(game::hks::lua_function); + function(game::hks::cclosure*, game::hks::HksObjectType); + + template + function(F f) + { + this->ptr = ui_scripting::convert_function(f); + this->type = game::hks::TCFUNCTION; + } + + 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; + + arguments operator()(const arguments& arguments) const; + + template + arguments operator()(T... arguments) const + { + return this->call({arguments...}); + } + + arguments operator()() const; + + game::hks::cclosure* ptr; + game::hks::HksObjectType type; + + private: + void add(); + void release(); + + int ref{}; + }; +} diff --git a/src/client/resource.hpp b/src/client/resource.hpp index 2ff1bf6a..0ed778c7 100644 --- a/src/client/resource.hpp +++ b/src/client/resource.hpp @@ -12,3 +12,10 @@ #define RUNNER 305 #define ICON_IMAGE 306 + +/* +#define LUI_COMMON 307 +#define LUI_UPDATER 308 + +#define LUA_JSON 309 +*/ diff --git a/src/common/utils/http.cpp b/src/common/utils/http.cpp index 3cb59991..8c0753f4 100644 --- a/src/common/utils/http.cpp +++ b/src/common/utils/http.cpp @@ -4,45 +4,117 @@ namespace utils::http { - std::optional get_data(const std::string& url) + namespace { - CComPtr stream; - - if (FAILED(URLOpenBlockingStreamA(nullptr, url.data(), &stream, 0, nullptr))) + struct progress_helper { - return {}; - } + const std::function* callback{}; + std::exception_ptr exception{}; + }; - char buffer[0x1000]; - std::string result; - - HRESULT status{}; - - do + int progress_callback(void* clientp, const curl_off_t dltotal, const curl_off_t dlnow, const curl_off_t /*ultotal*/, const curl_off_t /*ulnow*/) { - DWORD bytes_read = 0; - status = stream->Read(buffer, sizeof(buffer), &bytes_read); + auto* helper = static_cast(clientp); - if (bytes_read > 0) + try { - result.append(buffer, bytes_read); + if (*helper->callback && (*helper->callback)(dltotal, dlnow) == -1) + { + return -1; + } + } + catch (...) + { + helper->exception = std::current_exception(); + return -1; } - } - while (SUCCEEDED(status) && status != S_FALSE); - if (FAILED(status)) + return 0; + } + + size_t write_callback(void* contents, const size_t size, const size_t nmemb, void* userp) { - return {}; - } + auto* buffer = static_cast(userp); - return {result}; + const auto total_size = size * nmemb; + buffer->append(static_cast(contents), total_size); + return total_size; + } } - std::future> get_data_async(const std::string& url) + std::optional get_data(const std::string& url, const std::string& fields, + const headers& headers, const std::function& callback, int timeout) { - return std::async(std::launch::async, [url]() + curl_slist* header_list = nullptr; + auto* curl = curl_easy_init(); + if (!curl) { - return get_data(url); + return {}; + } + + auto _ = gsl::finally([&]() + { + curl_slist_free_all(header_list); + curl_easy_cleanup(curl); + }); + + for (const auto& header : headers) + { + auto data = header.first + ": " + header.second; + header_list = curl_slist_append(header_list, data.data()); + } + + std::string buffer{}; + progress_helper helper{}; + helper.callback = &callback; + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); + curl_easy_setopt(curl, CURLOPT_URL, url.data()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &helper); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); + + curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout); + + if (!fields.empty()) + { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, fields.data()); + } + + const auto code = curl_easy_perform(curl); + unsigned int response_code{}; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + if (code == CURLE_OK) + { + result result; + result.code = code; + result.response_code = response_code; + result.buffer = std::move(buffer); + + return result; + } + + if (helper.exception) + { + std::rethrow_exception(helper.exception); + } + + result result; + result.code = code; + + return result; + } + + std::future> get_data_async(const std::string& url, const std::string& fields, + const headers& headers, const std::function& callback) + { + return std::async(std::launch::async, [url, fields, headers, callback]() + { + return get_data(url, fields, headers, callback); }); } } diff --git a/src/common/utils/http.hpp b/src/common/utils/http.hpp index 65628a9f..0207ff22 100644 --- a/src/common/utils/http.hpp +++ b/src/common/utils/http.hpp @@ -3,9 +3,23 @@ #include #include #include +#include + +#include namespace utils::http { - std::optional get_data(const std::string& url); - std::future> get_data_async(const std::string& url); + struct result + { + CURLcode code{}; + unsigned int response_code{}; + std::string buffer{}; + }; + + using headers = std::unordered_map; + + std::optional get_data(const std::string& url, const std::string& fields = {}, + const headers& headers = {}, const std::function& callback = {}, int timeout = 0); + std::future> get_data_async(const std::string& url, const std::string& fields = {}, + const headers& headers = {}, const std::function& callback = {}); } diff --git a/src/common/utils/properties.cpp b/src/common/utils/properties.cpp new file mode 100644 index 00000000..953f041c --- /dev/null +++ b/src/common/utils/properties.cpp @@ -0,0 +1,24 @@ +#include "io.hpp" +#include "properties.hpp" +#include +#include + +namespace utils::properties +{ + std::filesystem::path get_appdata_path() + { + PWSTR path; + if (!SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path))) + { + throw std::runtime_error("Failed to read APPDATA path!"); + } + + auto _ = gsl::finally([&path] + { + CoTaskMemFree(path); + }); + + static auto appdata = std::filesystem::path(path) / "iw7-mod"; + return appdata; + } +} diff --git a/src/common/utils/properties.hpp b/src/common/utils/properties.hpp new file mode 100644 index 00000000..103e6ae6 --- /dev/null +++ b/src/common/utils/properties.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace utils::properties +{ + std::filesystem::path get_appdata_path(); +}