#include #include "loader/component_loader.hpp" #include "game/game.hpp" #include "game/dvars.hpp" #include "scheduler.hpp" #include "command.hpp" #include "filesystem.hpp" #include "scripting.hpp" #include "fastfiles.hpp" #include "mods.hpp" #include "updater.hpp" #include "console.hpp" #include "language.hpp" #include "config.hpp" #include "motd.hpp" #include "game/ui_scripting/execution.hpp" #include "game/scripting/execution.hpp" #include "ui_scripting.hpp" #include #include #include namespace ui_scripting { namespace { 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); std::unordered_map> converted_functions; utils::hook::detour hks_start_hook; utils::hook::detour hks_shutdown_hook; utils::hook::detour hks_package_require_hook; 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; } table get_globals() { const auto state = *game::hks::lua_state; return state->globals.v.table; } 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); } } } script_value json_to_lua(const nlohmann::json& json) { if (json.is_object()) { table object; for (const auto& [key, value] : json.items()) { object[key] = json_to_lua(value); } return object; } if (json.is_array()) { table array; auto index = 1; for (const auto& value : json.array()) { array[index++] = json_to_lua(value); } return array; } if (json.is_boolean()) { return json.get(); } if (json.is_number_integer()) { return json.get(); } if (json.is_number_float()) { return json.get(); } if (json.is_string()) { return json.get(); } return {}; } nlohmann::json lua_to_json(const script_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.get_raw().t == game::hks::TNIL) { return {}; } throw std::runtime_error("lua value must be of primitive type (boolean, integer, float, string)"); } void setup_functions() { const auto lua = get_globals(); lua["io"] = table(); lua["io"]["fileexists"] = filesystem::safe_io_func(utils::io::file_exists); lua["io"]["writefile"] = filesystem::safe_write_file; lua["io"]["filesize"] = filesystem::safe_io_func(utils::io::file_size); lua["io"]["createdirectory"] = filesystem::safe_io_func(utils::io::create_directory); lua["io"]["directoryexists"] = filesystem::safe_io_func(utils::io::directory_exists); lua["io"]["directoryisempty"] = filesystem::safe_io_func(utils::io::directory_is_empty); lua["io"]["listfiles"] = filesystem::safe_io_func>(utils::io::list_files); lua["io"]["removefile"] = filesystem::safe_io_func(utils::io::remove_file); lua["io"]["removedirectory"] = filesystem::safe_io_func(utils::io::remove_directory); lua["io"]["readfile"] = filesystem::safe_io_func( static_cast(utils::io::read_file)); auto language_table = table(); language_table["isnonlatin"] = language::is_non_latin; language_table["isslavic"] = language::is_slavic; language_table["isarabic"] = language::is_arabic; language_table["isasian"] = language::is_asian; language_table["iscustom"] = language::is_custom; lua["language"] = language_table; using game = table; auto game_type = game(); lua["game"] = game_type; 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["getweapondisplayname"] = [](const game&, const std::string& name) { const auto alternate = name.starts_with("alt_"); const auto weapon = ::game::G_GetWeaponForName(name.data()); char buffer[0x400] = {0}; ::game::CG_GetWeaponDisplayName(weapon, alternate, buffer, 0x400); return std::string(buffer); }; game_type["getloadedmod"] = [](const game&) { const auto& path = mods::get_mod(); return path.value_or(""); }; game_type["setlanguage"] = [](const game&, const std::string& language) { language::set(language); }; game_type["fastfileexists"] = [](const game&, const std::string& name) { return fastfiles::exists(name); }; game_type["openlink"] = [](const game&, const std::string& name) { const auto links = motd::get_links(); const auto link = links.find(name); if (link == links.end()) { return; } ShellExecuteA(nullptr, "open", link->second.data(), nullptr, nullptr, SW_SHOWNORMAL); }; game_type["getlinkurl"] = [](const game&, const std::string& name) -> script_value { const auto links = motd::get_links(); const auto link = links.find(name); if (link == links.end()) { return script_value(); } return link->second; }; game_type["islink"] = [](const game&, const std::string& name) { const auto links = motd::get_links(); const auto link = links.find(name); if (link == links.end()) { return false; } return true; }; lua["string"]["escapelocalization"] = [](const std::string& str) { return "\x1F"s.append(str); }; lua["string"]["el"] = lua["string"]["escapelocalization"]; lua["Engine"]["SetLanguage"] = [](const int index) { language::set_from_index(index); updater::relaunch(); }; using player = table; auto player_type = player(); lua["player"] = player_type; player_type["notify"] = [](const player&, const std::string& name, const variadic_args& va) { if (!::game::CL_IsCgameInitialized() || !::game::g_entities[0].client) { throw std::runtime_error("Not in game"); } const auto to_string = get_globals()["tostring"]; const auto arguments = get_return_values(); std::vector args{}; for (const auto& value : va) { const auto value_str = to_string(value); args.push_back(value_str[0].as()); } ::scheduler::once([name, args]() { try { std::vector arguments{}; for (const auto& arg : args) { arguments.push_back(arg); } const auto player = scripting::call("getentbynum", {0}).as(); scripting::notify(player, name, arguments); } catch (...) { } }, ::scheduler::pipeline::server); }; auto updater_table = table(); lua["updater"] = updater_table; updater_table["relaunch"] = updater::relaunch; updater_table["sethastriedupdate"] = updater::set_has_tried_update; updater_table["gethastriedupdate"] = updater::get_has_tried_update; updater_table["autoupdatesenabled"] = updater::auto_updates_enabled; updater_table["startupdatecheck"] = updater::start_update_check; updater_table["isupdatecheckdone"] = updater::is_update_check_done; updater_table["getupdatecheckstatus"] = updater::get_update_check_status; updater_table["isupdateavailable"] = updater::is_update_available; updater_table["startupdatedownload"] = updater::start_update_download; updater_table["isupdatedownloaddone"] = updater::is_update_download_done; updater_table["getupdatedownloadstatus"] = updater::get_update_download_status; updater_table["cancelupdate"] = updater::cancel_update; updater_table["isrestartrequired"] = updater::is_restart_required; updater_table["getlasterror"] = updater::get_last_error; updater_table["getcurrentfile"] = updater::get_current_file; auto mods_table = table(); lua["mods"] = mods_table; mods_table["getloaded"] = []() -> script_value { const auto& mod = mods::get_mod(); if (mod.has_value()) { return mod.value(); } return {}; }; mods_table["getlist"] = mods::get_mod_list; mods_table["getinfo"] = [](const std::string& mod) { table info_table{}; const auto info = mods::get_mod_info(mod); const auto has_value = info.has_value(); info_table["isvalid"] = has_value; if (!has_value) { return info_table; } const auto& map = info.value(); for (const auto& [key, value] : map.items()) { info_table[key] = json_to_lua(value); } return info_table; }; auto mods_stats_table = table(); mods_table["stats"] = mods_stats_table; mods_stats_table["set"] = [](const std::string& key, const script_value& value) { const auto json_value = lua_to_json(value); mods::get_current_stats()[key] = json_value; mods::write_mod_stats(); }; mods_stats_table["get"] = [](const std::string& key) { return json_to_lua(mods::get_current_stats()); }; mods_stats_table["mapset"] = [](const std::string& mapname, const std::string& key, const script_value& value) { const auto json_value = lua_to_json(value); auto& stats = mods::get_current_stats(); stats["maps"][mapname][key] = json_value; mods::write_mod_stats(); }; mods_stats_table["mapget"] = [](const std::string& mapname, const std::string& key) { auto& stats = mods::get_current_stats(); return json_to_lua(stats["maps"][mapname][key]); }; mods_stats_table["save"] = mods::write_mod_stats; mods_stats_table["getall"] = []() { return json_to_lua(mods::get_current_stats()); }; mods_stats_table["setfromjson"] = [](const std::string& data) { const auto json = nlohmann::json::parse(data); mods::get_current_stats() = json; }; auto config_table = table(); lua["config"] = config_table; config_table["get"] = [](const std::string& key, const variadic_args& va) -> script_value { const auto value = config::get_raw(key); return json_to_lua(value); }; config_table["getstring"] = [](const std::string& key, const variadic_args& va) -> script_value { const auto default_value = va.size() >= 1 ? va[0] : script_value(""); const auto value = config::get(key); if (!value.has_value()) { return default_value; } const auto& json = value.value(); if (!json.is_string()) { return default_value; } return json.get(); }; config_table["getint"] = [](const std::string& key, const variadic_args& va) -> script_value { const auto default_value = va.size() >= 1 ? va[0] : script_value(0); const auto value = config::get(key); if (!value.has_value()) { return default_value; } const auto& json = value.value(); if (!json.is_number_integer()) { return default_value; } return json.get(); }; config_table["getfloat"] = [](const std::string& key, const variadic_args& va) -> script_value { const auto default_value = va.size() >= 1 ? va[0] : script_value(0.f); const auto value = config::get(key); if (!value.has_value()) { return default_value; } const auto& json = value.value(); if (!json.is_number()) { return default_value; } return json.get(); }; config_table["getbool"] = [](const std::string& key, const variadic_args& va) -> script_value { const auto default_value = va.size() >= 1 ? va[0] : script_value(false); const auto value = config::get(key); if (!value.has_value()) { return default_value; } const auto& json = value.value(); if (!json.is_boolean()) { return default_value; } return json.get(); }; config_table["set"] = [](const std::string& key, const script_value& value) { config::set(key, lua_to_json(value)); }; auto motd_table = table(); lua["motd"] = motd_table; motd_table["getnumfeaturedtabs"] = motd::get_num_featured_tabs; motd_table["getmotd"] = []() { return json_to_lua(motd::get_motd()); }; motd_table["getfeaturedtab"] = [](const int index) { return json_to_lua(motd::get_featured_tab(index)); }; motd_table["hasseentoday"] = []() { const auto last_seen = config::get("motd_last_seen"); if (!last_seen.has_value()) { return false; } const auto value = static_cast(last_seen.value()); const auto before = std::chrono::floor(std::chrono::system_clock::from_time_t(value)); const auto now = std::chrono::floor(std::chrono::system_clock::now()); const auto diff = std::chrono::sys_days{now} - std::chrono::sys_days{before}; return diff.count() < 1; }; motd_table["sethasseentoday"] = []() { const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); config::set("motd_last_seen", static_cast(now)); }; } void start() { globals = {}; const auto lua = get_globals(); lua["EnableGlobals"](); table keydown_event{}; keydown_event["key"] = ""; keydown_event["keynum"] = 0; table keyup_event{}; keyup_event["key"] = ""; keyup_event["keynum"] = 0; lua["LUI"]["CachedEvents"]["keydown"] = keydown_event; lua["LUI"]["CachedEvents"]["keyup"] = keyup_event; 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) { const auto _0 = gsl::finally(&try_start); return hks_start_hook.invoke(a1); } void hks_shutdown_stub() { converted_functions.clear(); globals = {}; 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_xasset_header_stub(game::XAssetType type, const char* name, int allow_create_default) { 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("/\\")); std::string name_ = utils::string::replace(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; return static_cast(reinterpret_cast(1)); } else if (name_.starts_with("ui/LUI/")) { return game::DB_FindXAssetHeader(type, name, allow_create_default); } return static_cast(nullptr); } 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 utils::hook::invoke(0x1402D9410, 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; } int removed_function_stub(game::hks::lua_State* /*state*/) { return 0; } } 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; } class component final : public component_interface { public: void post_unpack() override { utils::hook::call(0x14030BF2B, db_find_xasset_header_stub); utils::hook::call(0x14030C079, db_find_xasset_header_stub); utils::hook::call(0x14030C104, hks_load_stub); hks_package_require_hook.create(0x1402B4DA0, hks_package_require_stub); hks_start_hook.create(0x140328BE0, hks_start_stub); hks_shutdown_hook.create(0x1403203B0, hks_shutdown_stub); // remove unsafe functions utils::hook::nop(0x1402D885A, 1); utils::hook::jump(0x14031E700, 0x1402D86E0); utils::hook::jump(0x1402BFCC0, removed_function_stub); // io utils::hook::jump(0x14017EE60, removed_function_stub); // profile utils::hook::jump(0x1402C0150, removed_function_stub); // os utils::hook::jump(0x14017F730, removed_function_stub); // serialize utils::hook::jump(0x1402C0FF0, removed_function_stub); // hks utils::hook::jump(0x14017EC60, removed_function_stub); // debug utils::hook::nop(0x1402BFC48, 5); // coroutine utils::hook::jump(0x1402B7FD0, removed_function_stub); utils::hook::jump(0x1402B7C40, removed_function_stub); utils::hook::jump(0x1402BAC30, removed_function_stub); utils::hook::jump(0x1402B5480, removed_function_stub); utils::hook::jump(0x1402C2030, removed_function_stub); utils::hook::jump(0x1402C2180, removed_function_stub); utils::hook::jump(0x1402B6F70, removed_function_stub); utils::hook::jump(0x1402BD890, removed_function_stub); } }; } REGISTER_COMPONENT(ui_scripting::component)