t7x/src/client/component/ui_scripting.cpp
2023-02-18 19:38:29 +01:00

494 lines
16 KiB
C++

#include <std_include.hpp>
#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 <utils/hook.hpp>
#include <utils/io.hpp>
#include <utils/finally.hpp>
namespace ui_scripting
{
namespace
{
std::unordered_map<game::hks::cclosure*, std::function<arguments(const function_arguments& args)>>
converted_functions;
utils::hook::detour ui_cod_init_hook;
utils::hook::detour ui_cod_lobbyui_init_hook;
utils::hook::detour ui_shutdown_hook;
utils::hook::detour hks_package_require_hook;
utils::hook::detour lua_cod_getrawfile_hook;
game::dvar_t* dvar_cg_enable_unsafe_lua_functions;
static bool unsafe_function_called_message_shown = false;
struct globals_t
{
std::string in_require_script;
std::unordered_map<std::string, std::string> loaded_scripts;
std::unordered_map<std::string, std::string> 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<function>())
{
const auto results = lua["pcall"](load_results);
if (!results[0].as<bool>())
{
print_error(results[1].as<std::string>());
}
}
else if (load_results[1].is<std::string>())
{
print_error(load_results[1].as<std::string>());
}
}
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();
using game = table;
auto game_type = game();
lua["game"] = game_type;
}
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<game::hks::lua_function>(0x141D30290_g)); // hks::base_print
lua["table"]["unpack"] = lua["unpack"];
lua["luiglobals"] = lua;
utils::nt::library host{};
load_scripts((game::get_appdata_path() / "data/ui_scripts/").string());
load_scripts((host.get_folder() / "boiii/ui_scripts/").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(bool frontend)
{
ui_cod_init_hook.invoke(frontend);
if (game::Com_IsRunningUILevel())
{
// Fetch the names of the local files so file overrides are already handled
globals = {};
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 ui_shutdown_stub()
{
converted_functions.clear();
globals = {};
return ui_shutdown_hook.invoke<void>();
}
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<void*>(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<int>(0x141D3AFB0_g, state, compiler_options, reader, reader_data, debug_reader,
debug_reader_data, chunk_name);
}
game::XAssetHeader lua_cod_getrawfile_stub(char* filename)
{
game::XAssetHeader header{.luaFile = nullptr};
if (!is_loaded_script(globals.in_require_script) && !is_local_script(filename))
{
return lua_cod_getrawfile_hook.invoke<game::XAssetHeader>(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;
header.luaFile = reinterpret_cast<game::LuaFile*>(1);
return header;
}
return lua_cod_getrawfile_hook.invoke<game::XAssetHeader>(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, open the following file in your Black Ops 3 installation: players/user/config.cfg.\nIn this file change 'set cg_enable_unsafe_lua_functions 0' to 'set cg_enable_unsafe_lua_functions 1' and restart Black Ops 3.",
"Unsafe lua function called");
unsafe_function_called_message_shown = true;
}
return 0;
}
};
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<int>(results.size());
}
catch (const std::exception& ex)
{
game::hks::hksi_luaL_error(state, ex.what());
}
return 0;
}
template <typename F>
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 client_component
{
public:
void post_unpack() override
{
utils::hook::call(0x141D4979A_g, hks_load_stub);
hks_package_require_hook.create(0x141D28EF0_g, hks_package_require_stub);
ui_cod_init_hook.create(0x141F298B0_g, ui_cod_init_stub);
ui_cod_lobbyui_init_hook.create(0x141F2C620_g, ui_cod_lobbyui_init_stub);
ui_shutdown_hook.create(0x14270E9C0_g, ui_shutdown_stub);
lua_cod_getrawfile_hook.create(0x141F0F880_g, lua_cod_getrawfile_stub);
dvar_cg_enable_unsafe_lua_functions = game::Dvar_RegisterBool(
game::Dvar_GenerateHash("cg_enable_unsafe_lua_functions"), "cg_enable_unsafe_lua_functions", false,
static_cast<game::dvarFlags_e>(0x1000), "Enables the use of unsafe lua functions");
dvar_cg_enable_unsafe_lua_functions->debugName = "cg_enable_unsafe_lua_functions";
scheduler::once([]()
{
game::dvar_t* dvar_callstack_ship = game::Dvar_FindVar("ui_error_callstack_ship");
dvar_callstack_ship->flags = (game::dvarFlags_e)0;
game::dvar_t* dvar_report_delay = game::Dvar_FindVar("ui_error_report_delay");
dvar_report_delay->flags = (game::dvarFlags_e)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", [](auto& params)
{
auto frontend = game::Com_IsRunningUILevel();
if (frontend)
{
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);
}
});
scheduler::once([]()
{
if (!dvar_cg_enable_unsafe_lua_functions->current.enabled)
{
// 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
}
}, scheduler::pipeline::dvars_loaded);
}
};
}
REGISTER_COMPONENT(ui_scripting::component)