#include #include "loader/component_loader.hpp" #include "game/game.hpp" #include "game/ui_scripting/execution.hpp" #include "command.hpp" #include "ui_scripting.hpp" #include "scheduler.hpp" #include #include #include #include namespace ui_scripting { namespace { std::unordered_map> converted_functions; utils::hook::detour ui_cod_init_hook; utils::hook::detour ui_cod_lobbyui_init_hook; utils::hook::detour cl_first_snapshot_hook; utils::hook::detour ui_shutdown_hook; utils::hook::detour hks_package_require_hook; utils::hook::detour lua_cod_getrawfile_hook; bool unsafe_function_called_message_shown = false; struct globals_t { std::string in_require_script; std::unordered_map loaded_scripts; std::unordered_map local_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); } bool is_local_script(const std::string& name) { return globals.local_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) { printf("************** LUI script execution error **************\n"); printf("%s\n", error.data()); printf("********************************************************\n"); } void print_loading_script(const std::string& name) { printf("Loading LUI script '%s'\n", name.data()); } std::string get_current_script(game::hks::lua_State* 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 = utils::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 state = *game::hks::lua_state; const auto lua = get_globals(); state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_ON; const auto load_results = lua["loadstring"](data, name); state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_SECURE; 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_local_script_files(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) { const auto script_file = script.generic_string(); if (std::filesystem::is_regular_file(script)) { const std::string file_path = script_file.substr(script_file.find("ui_scripts") + 11); globals.local_scripts[file_path] = script_file; } else if (std::filesystem::is_directory(script)) { load_local_script_files(script_file); } } } void load_scripts(const std::string& script_dir) { if (!utils::io::directory_exists(script_dir)) { return; } load_local_script_files(script_dir); const auto scripts = utils::io::list_files(script_dir); for (const auto& script : scripts) { std::string data; const auto script_file = script.generic_string(); if (std::filesystem::is_directory(script) && utils::io::read_file(script_file + "/__init__.lua", &data)) { print_loading_script(script_file); load_script(script_file + "/__init__.lua", data); } } } void setup_functions() { const auto lua = get_globals(); lua["game"] = table(); } 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"; const auto state = *game::hks::lua_state; state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_ON; lua["loadstring"](code)[0](); state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_SECURE; } void start() { globals = {}; const auto lua = get_globals(); enable_globals(); setup_functions(); lua["print"] = function(reinterpret_cast(0x141D30290_g)); // hks::base_print lua["table"]["unpack"] = lua["unpack"]; lua["luiglobals"] = lua; const utils::nt::library host{}; const auto folder = game::is_server() ? "lobby_scripts/" : "ui_scripts/"; load_scripts((game::get_appdata_path() / "data" / folder).string()); load_scripts((host.get_folder() / "boiii" / folder).string()); } void try_start() { try { start(); } catch (const std::exception& ex) { printf("Failed to load LUI scripts: %s\n", ex.what()); } } void ui_cod_init_stub(const bool frontend) { ui_cod_init_hook.invoke(frontend); if (!game::is_server() && game::Com_IsRunningUILevel()) { // Fetch the names of the local files so file overrides are already handled globals = {}; const utils::nt::library host{}; load_local_script_files((game::get_appdata_path() / "data/ui_scripts/").string()); load_local_script_files((host.get_folder() / "boiii/ui_scripts/").string()); return; } const auto _0 = utils::finally(&try_start); } void ui_cod_lobbyui_init_stub() { ui_cod_lobbyui_init_hook.invoke(); const auto _0 = utils::finally(&try_start); } void cl_first_snapshot_stub(int a1) { cl_first_snapshot_hook.invoke(a1); if (game::Com_IsRunningUILevel()) { return; } const auto _0 = utils::finally(&try_start); } void ui_shutdown_stub() { converted_functions.clear(); globals = {}; return ui_shutdown_hook.invoke(); } void* hks_package_require_stub(game::hks::lua_State* state) { const auto script = get_current_script(state); const auto root = get_root_script(script); globals.in_require_script = root; return hks_package_require_hook.invoke(state); } int hks_load_stub(game::hks::lua_State* state, void* compiler_options, void* reader, void* reader_data, void* debug_reader, void* debug_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(game::select(0x141D3AFB0, 0x1403E4090), state, compiler_options, reader, reader_data, debug_reader, debug_reader_data, chunk_name); } game::XAssetHeader lua_cod_getrawfile_stub(char* filename) { if (!is_loaded_script(globals.in_require_script) && !is_local_script(filename)) { return lua_cod_getrawfile_hook.invoke(filename); } const std::string name_ = filename; std::string target_script; if (is_loaded_script(globals.in_require_script)) { const auto folder = globals.in_require_script.substr(0, globals.in_require_script.find_last_of("/\\")); target_script = folder + "/" + name_ + ".lua"; } else { target_script = globals.local_scripts[name_]; } if (utils::io::file_exists(target_script)) { globals.load_raw_script = true; globals.raw_script_name = target_script; return game::XAssetHeader{ .luaFile = reinterpret_cast(1) // }; } return lua_cod_getrawfile_hook.invoke(filename); } int luaopen_stub([[maybe_unused]] game::hks::lua_State* l) { return 0; } int lua_unsafe_function_stub([[maybe_unused]] game::hks::lua_State* l) { if (!unsafe_function_called_message_shown) { auto state = get_globals(); // TODO: Is it possible to do this with a confirm dialog? Doing this in LUI seems unsafe to me because mods will be able to change this aswell state["LuaUtils"]["ShowMessageDialog"]( 0, 0, "The map/mod you are playing tried to run code that can be unsafe. This can include writing or reading files on your system, accessing environment variables, running system commands or loading a dll. These are usually used for storing data across games, integrating third party software like Discord or fetching data from a server to make the gameplay for dynamic.\nThis can also cause a lot of harm by the wrong people.\n\nIf you trust this map/mod and want to enable these features, restart Black Ops 3 with the -unsafe-lua commandline argument.", "Unsafe lua function called"); unsafe_function_called_message_shown = true; } return 0; } void patch_unsafe_lua_functions() { if (utils::flags::has_flag("unsafe-lua")) { return; } // Do not allow the HKS vm to open LUA's libraries // Disable unsafe functions utils::hook::jump(0x141D34190_g, luaopen_stub); // debug utils::hook::jump(0x141D300B0_g, lua_unsafe_function_stub); // base_loadfile utils::hook::jump(0x141D31EE0_g, lua_unsafe_function_stub); // base_load utils::hook::jump(0x141D2CF00_g, lua_unsafe_function_stub); // string_dump utils::hook::jump(0x141FD3220_g, lua_unsafe_function_stub); // engine_openurl utils::hook::jump(0x141D2AFF0_g, lua_unsafe_function_stub); // os_getenv utils::hook::jump(0x141D2B790_g, lua_unsafe_function_stub); // os_exit utils::hook::jump(0x141D2B7C0_g, lua_unsafe_function_stub); // os_remove utils::hook::jump(0x141D2BB70_g, lua_unsafe_function_stub); // os_rename utils::hook::jump(0x141D2B360_g, lua_unsafe_function_stub); // os_tmpname utils::hook::jump(0x141D2B0F0_g, lua_unsafe_function_stub); // os_sleep utils::hook::jump(0x141D2AF90_g, lua_unsafe_function_stub); // os_execute utils::hook::jump(0x141D2AFF0_g, lua_unsafe_function_stub); // os_getenv // io helpers utils::hook::jump(0x141D32390_g, lua_unsafe_function_stub); // io_tostring utils::hook::jump(0x141D2FDC0_g, lua_unsafe_function_stub); // io_close_file utils::hook::jump(0x141D2FD50_g, lua_unsafe_function_stub); // io_flush utils::hook::jump(0x141D31260_g, lua_unsafe_function_stub); // io_lines utils::hook::jump(0x141D305C0_g, lua_unsafe_function_stub); // io_read_file utils::hook::jump(0x141D305C0_g, lua_unsafe_function_stub); // io_read_file utils::hook::jump(0x141D320A0_g, lua_unsafe_function_stub); // io_seek_file utils::hook::jump(0x141D321E0_g, lua_unsafe_function_stub); // io_setvbuf utils::hook::jump(0x141D2FCD0_g, lua_unsafe_function_stub); // io_write // io functions utils::hook::jump(0x141D2FD10_g, lua_unsafe_function_stub); // io_write utils::hook::jump(0x141D30F40_g, lua_unsafe_function_stub); // io_read utils::hook::jump(0x141D2FF00_g, lua_unsafe_function_stub); // io_close utils::hook::jump(0x141D2FD90_g, lua_unsafe_function_stub); // io_flush utils::hook::jump(0x141D313A0_g, lua_unsafe_function_stub); // io_lines utils::hook::jump(0x141D31BA0_g, lua_unsafe_function_stub); // io_input utils::hook::jump(0x141D31BC0_g, lua_unsafe_function_stub); // io_output utils::hook::jump(0x141D31BE0_g, lua_unsafe_function_stub); // io_type utils::hook::jump(0x141D31DD0_g, lua_unsafe_function_stub); // io_open utils::hook::jump(0x141D31D70_g, lua_unsafe_function_stub); // io_tmpfile utils::hook::jump(0x141D33C00_g, lua_unsafe_function_stub); // io_popen utils::hook::jump(0x141D2D0C0_g, lua_unsafe_function_stub); // serialize_persist utils::hook::jump(0x141D2D480_g, lua_unsafe_function_stub); // serialize_unpersist utils::hook::jump(0x141D2F560_g, lua_unsafe_function_stub); // havokscript_compiler_settings utils::hook::jump(0x141D2F660_g, lua_unsafe_function_stub); // havokscript_setgcweights utils::hook::jump(0x141D2FB10_g, lua_unsafe_function_stub); // havokscript_getgcweights utils::hook::jump(0x141D299C0_g, lua_unsafe_function_stub); // package_loadlib } } int main_handler(game::hks::lua_State* state) { const auto value = state->m_apistack.base[-1]; if (value.t != game::hks::TCFUNCTION) { return 0; } const auto closure = value.v.cClosure; if (!converted_functions.contains(closure)) { return 0; } const auto& function = converted_functions[closure]; try { 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& ex) { game::hks::hksi_luaL_error(state, ex.what()); } 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 generic_component { public: void post_unpack() override { utils::hook::call(game::select(0x141D4979A, 0x1403F233A), hks_load_stub); hks_package_require_hook.create(game::select(0x141D28EF0, 0x1403D7FC0), hks_package_require_stub); ui_cod_init_hook.create(game::UI_CoD_Init, ui_cod_init_stub); ui_cod_lobbyui_init_hook.create(game::UI_CoD_LobbyUI_Init, ui_cod_lobbyui_init_stub); ui_shutdown_hook.create(game::UI_CoD_Shutdown, ui_shutdown_stub); lua_cod_getrawfile_hook.create(game::select(0x141F0EFE0, 0x1404BCB70), lua_cod_getrawfile_stub); if (game::is_server()) { return; } cl_first_snapshot_hook.create(0x141320E60_g, cl_first_snapshot_stub); scheduler::once([]() { game::dvar_t* dvar_callstack_ship = game::Dvar_FindVar("ui_error_callstack_ship"); dvar_callstack_ship->flags = static_cast(0); game::dvar_t* dvar_report_delay = game::Dvar_FindVar("ui_error_report_delay"); dvar_report_delay->flags = static_cast(0); game::Dvar_SetFromStringByName("ui_error_callstack_ship", "1", true); game::Dvar_SetFromStringByName("ui_error_report_delay", "0", true); }, scheduler::pipeline::renderer); command::add("luiReload", [] { if (game::Com_IsRunningUILevel()) { converted_functions.clear(); globals.loaded_scripts.clear(); globals.local_scripts.clear(); game::UI_CoD_Shutdown(); game::UI_CoD_Init(true); // Com_LoadFrontEnd stripped game::Lua_CoD_LoadLuaFile(*game::hks::lua_state, "ui_mp.T6.main"); game::UI_AddMenu(game::UI_CoD_GetRootNameForController(0), "main", -1, *game::hks::lua_state); game::UI_CoD_LobbyUI_Init(); } else { // TODO: Find a way to do a full shutdown & restart like in frontend, that opens up the loading screen that can't be easily closed game::CG_LUIHUDRestart(0); } }); patch_unsafe_lua_functions(); } }; } REGISTER_COMPONENT_WORKING(ui_scripting::component)