From b0a25ec0df227aedc0f417e695892f923bb2b24a Mon Sep 17 00:00:00 2001 From: Federico Cecchetto Date: Fri, 10 Dec 2021 00:20:40 +0100 Subject: [PATCH] Support calling & hooking functions in GSC scripts --- src/client/component/logfile.cpp | 91 ++++++++++++ src/client/component/logfile.hpp | 5 + src/client/component/scripting.cpp | 34 +++++ src/client/component/scripting.hpp | 6 + src/client/game/scripting/execution.cpp | 25 ++++ src/client/game/scripting/execution.hpp | 3 + src/client/game/scripting/function_tables.cpp | 13 ++ src/client/game/scripting/functions.cpp | 13 ++ src/client/game/scripting/functions.hpp | 2 + src/client/game/scripting/lua/context.cpp | 135 +++++++++++++++++- .../game/scripting/lua/value_conversion.cpp | 67 ++++++--- src/client/game/symbols.hpp | 1 + 12 files changed, 374 insertions(+), 21 deletions(-) create mode 100644 src/client/component/scripting.hpp diff --git a/src/client/component/logfile.cpp b/src/client/component/logfile.cpp index 200178a..0134123 100644 --- a/src/client/component/logfile.cpp +++ b/src/client/component/logfile.cpp @@ -13,6 +13,8 @@ namespace logfile { + std::unordered_map vm_execute_hooks; + namespace { utils::hook::detour scr_player_killed_hook; @@ -21,6 +23,10 @@ namespace logfile std::vector player_killed_callbacks; std::vector player_damage_callbacks; + utils::hook::detour vm_execute_hook; + char empty_function[2] = {0x32, 0x34}; // CHECK_CLEAR_PARAMS, END + bool hook_enabled = true; + sol::lua_value convert_entity(lua_State* state, const game::mp::gentity_s* ent) { if (!ent) @@ -182,6 +188,78 @@ namespace logfile // G_ShutdownGame return reinterpret_cast(0x1402F8C10)(freeScripts); } + + unsigned int local_id_to_entity(unsigned int local_id) + { + const auto variable = game::scr_VarGlob->objectVariableValue[local_id]; + return variable.u.f.next; + } + + bool execute_vm_hook(const char* pos) + { + if (vm_execute_hooks.find(pos) == vm_execute_hooks.end()) + { + hook_enabled = true; + return false; + } + + if (!hook_enabled && pos > reinterpret_cast(vm_execute_hooks.size())) + { + hook_enabled = true; + return false; + } + + const auto hook = vm_execute_hooks[pos]; + const auto state = hook.lua_state(); + + const scripting::entity self = local_id_to_entity(game::scr_VmPub->function_frame->fs.localId); + + std::vector args; + + const auto top = game::scr_function_stack->top; + + for (auto* value = top; value->type != game::SCRIPT_END; --value) + { + args.push_back(scripting::lua::convert(state, *value)); + } + + const auto result = hook(self, sol::as_args(args)); + scripting::lua::handle_error(result); + + return true; + } + + void vm_execute_stub(utils::hook::assembler& a) + { + const auto replace = a.newLabel(); + const auto end = a.newLabel(); + + a.pushad64(); + + a.mov(rcx, r14); + a.call_aligned(execute_vm_hook); + + a.cmp(al, 0); + a.jne(replace); + + a.popad64(); + a.jmp(end); + + a.bind(end); + + a.movzx(r15d, byte_ptr(r14)); + a.inc(r14); + a.lea(eax, dword_ptr(r15, -0x17)); + a.mov(dword_ptr(rbp, 0x68), r15d); + + a.jmp(0x1403FA143); + + a.bind(replace); + + a.popad64(); + a.mov(r14, reinterpret_cast(empty_function)); + a.jmp(end); + } } void add_player_damage_callback(const sol::protected_function& callback) @@ -198,6 +276,17 @@ namespace logfile { player_damage_callbacks.clear(); player_killed_callbacks.clear(); + vm_execute_hooks.clear(); + } + + void enable_vm_execute_hook() + { + hook_enabled = true; + } + + void disable_vm_execute_hook() + { + hook_enabled = false; } class component final : public component_interface @@ -217,6 +306,8 @@ namespace logfile utils::hook::call(0x14043E550, g_shutdown_game_stub); utils::hook::call(0x14043EA11, g_shutdown_game_stub); + + utils::hook::jump(0x1403FA134, utils::hook::assemble(vm_execute_stub), true); } }; } diff --git a/src/client/component/logfile.hpp b/src/client/component/logfile.hpp index debf001..f0609a1 100644 --- a/src/client/component/logfile.hpp +++ b/src/client/component/logfile.hpp @@ -2,7 +2,12 @@ namespace logfile { + extern std::unordered_map vm_execute_hooks; + void add_player_damage_callback(const sol::protected_function& callback); void add_player_killed_callback(const sol::protected_function& callback); void clear_callbacks(); + + void enable_vm_execute_hook(); + void disable_vm_execute_hook(); } diff --git a/src/client/component/scripting.cpp b/src/client/component/scripting.cpp index 8277251..823ca6f 100644 --- a/src/client/component/scripting.cpp +++ b/src/client/component/scripting.cpp @@ -5,20 +5,29 @@ #include #include "game/scripting/entity.hpp" +#include "game/scripting/functions.hpp" #include "game/scripting/event.hpp" #include "game/scripting/lua/engine.hpp" #include "game/scripting/execution.hpp" #include "scheduler.hpp" +#include "scripting.hpp" namespace scripting { + std::unordered_map> script_function_table; + namespace { utils::hook::detour vm_notify_hook; utils::hook::detour scr_load_level_hook; utils::hook::detour g_shutdown_game_hook; + utils::hook::detour scr_set_thread_position_hook; + utils::hook::detour process_script_hook; + + std::string current_file; + void vm_notify_stub(const unsigned int notify_list_owner_id, const game::scr_string_t string_value, game::VariableValue* top) { @@ -62,6 +71,28 @@ namespace scripting lua::engine::stop(); return g_shutdown_game_hook.invoke(free_scripts); } + + void process_script_stub(const char* filename) + { + const auto file_id = atoi(filename); + if (file_id) + { + current_file = scripting::find_token(file_id); + } + else + { + current_file = filename; + } + + process_script_hook.invoke(filename); + } + + void scr_set_thread_position_stub(unsigned int threadName, const char* codePos) + { + const auto function_name = scripting::find_token(threadName); + script_function_table[current_file][function_name] = codePos; + scr_set_thread_position_hook.invoke(threadName, codePos); + } } class component final : public component_interface @@ -74,6 +105,9 @@ namespace scripting scr_load_level_hook.create(SELECT_VALUE(0x140005260, 0x140325B90), scr_load_level_stub); g_shutdown_game_hook.create(SELECT_VALUE(0x140228BA0, 0x1402F8C10), g_shutdown_game_stub); + scr_set_thread_position_hook.create(SELECT_VALUE(0x1403115E0, 0x1403EDB10), scr_set_thread_position_stub); + process_script_hook.create(SELECT_VALUE(0x14031AB30, 0x1403F7300), process_script_stub); + scheduler::loop([]() { lua::engine::run_frame(); diff --git a/src/client/component/scripting.hpp b/src/client/component/scripting.hpp new file mode 100644 index 0000000..864e09e --- /dev/null +++ b/src/client/component/scripting.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace scripting +{ + extern std::unordered_map> script_function_table; +} diff --git a/src/client/game/scripting/execution.cpp b/src/client/game/scripting/execution.cpp index e5c2375..ba4e4ab 100644 --- a/src/client/game/scripting/execution.cpp +++ b/src/client/game/scripting/execution.cpp @@ -3,6 +3,8 @@ #include "safe_execution.hpp" #include "stack_isolation.hpp" +#include "component/scripting.hpp" + namespace scripting { namespace @@ -130,6 +132,29 @@ namespace scripting return get_return_value(); } + const char* get_function_pos(const std::string& filename, const std::string& function) + { + if (scripting::script_function_table.find(filename) == scripting::script_function_table.end()) + { + throw std::runtime_error("File '" + filename + "' not found"); + }; + + const auto functions = scripting::script_function_table[filename]; + if (functions.find(function) == functions.end()) + { + throw std::runtime_error("Function '" + function + "' in file '" + filename + "' not found"); + } + + return functions.at(function); + } + + script_value call_script_function(const entity& entity, const std::string& filename, + const std::string& function, const std::vector& arguments) + { + const auto pos = get_function_pos(filename, function); + return exec_ent_thread(entity, pos, arguments); + } + static std::unordered_map> custom_fields; script_value get_custom_field(const entity& entity, const std::string& field) diff --git a/src/client/game/scripting/execution.hpp b/src/client/game/scripting/execution.hpp index 2780e27..7fff184 100644 --- a/src/client/game/scripting/execution.hpp +++ b/src/client/game/scripting/execution.hpp @@ -22,6 +22,9 @@ namespace scripting } script_value exec_ent_thread(const entity& entity, const char* pos, const std::vector& arguments); + const char* get_function_pos(const std::string& filename, const std::string& function); + script_value call_script_function(const entity& entity, const std::string& filename, + const std::string& function, const std::vector& arguments); void clear_entity_fields(const entity& entity); void clear_custom_fields(); diff --git a/src/client/game/scripting/function_tables.cpp b/src/client/game/scripting/function_tables.cpp index 7664578..6eac3d9 100644 --- a/src/client/game/scripting/function_tables.cpp +++ b/src/client/game/scripting/function_tables.cpp @@ -1518,5 +1518,18 @@ namespace scripting {"SetupCallbacks", 33531}, {"SetupDamageFlags", 33542}, {"struct", 36698}, + {"codescripts/delete", 0x053D}, + {"codescripts/struct", 0x053E}, + {"maps/mp/gametypes/_callbacksetup", 0x0540}, + {"codescripts/character", 0xA4E5}, + {"common_scripts/_artcommon", 42214}, + {"common_scripts/_bcs_location_trigs", 42215}, + {"common_scripts/_createfx", 42216}, + {"common_scripts/_createfxmenu", 42217}, + {"common_scripts/_destructible", 42218}, + {"common_scripts/_dynamic_world", 42219}, + {"maps/createart/mp_vlobby_room_art", 42735}, + {"maps/createart/mp_vlobby_room_fog", 42736}, + {"maps/createart/mp_vlobby_room_fog_hdr", 42737} }; } diff --git a/src/client/game/scripting/functions.cpp b/src/client/game/scripting/functions.cpp index 0489dcc..3452c76 100644 --- a/src/client/game/scripting/functions.cpp +++ b/src/client/game/scripting/functions.cpp @@ -71,6 +71,19 @@ namespace scripting } } + std::string find_token(unsigned int id) + { + for (const auto& token : token_map) + { + if (token.second == id) + { + return token.first; + } + } + + return utils::string::va("_ID%i", id); + } + unsigned int find_token_id(const std::string& name) { const auto result = token_map.find(name); diff --git a/src/client/game/scripting/functions.hpp b/src/client/game/scripting/functions.hpp index 2227753..0422bcf 100644 --- a/src/client/game/scripting/functions.hpp +++ b/src/client/game/scripting/functions.hpp @@ -9,6 +9,8 @@ namespace scripting using script_function = void(*)(game::scr_entref_t); + std::string find_token(unsigned int id); unsigned int find_token_id(const std::string& name); + script_function find_function(const std::string& name, const bool prefer_global); } diff --git a/src/client/game/scripting/lua/context.cpp b/src/client/game/scripting/lua/context.cpp index dbf400d..462a662 100644 --- a/src/client/game/scripting/lua/context.cpp +++ b/src/client/game/scripting/lua/context.cpp @@ -8,6 +8,7 @@ #include "../../../component/command.hpp" #include "../../../component/logfile.hpp" +#include "../../../component/scripting.hpp" #include @@ -204,6 +205,25 @@ namespace scripting::lua return scripting::lua::entity_to_struct(s, id); }; + entity_type["struct"] = sol::property([](const entity& entity, const sol::this_state s) + { + const auto id = entity.get_entity_id(); + return scripting::lua::entity_to_struct(s, id); + }); + + entity_type["scriptcall"] = [](const entity& entity, const sol::this_state s, const std::string& filename, + const std::string function, sol::variadic_args va) + { + std::vector arguments{}; + + for (auto arg : va) + { + arguments.push_back(convert({s, arg})); + } + + return convert(s, call_script_function(entity, filename, function, arguments)); + }; + struct game { }; @@ -269,7 +289,6 @@ namespace scripting::lua game_type["getgamevar"] = [](const sol::this_state s) { const auto id = *reinterpret_cast(0x14815DEB4); - const auto value = ::game::scr_VarGlob->childVariableValue[id]; ::game::VariableValue variable{}; @@ -278,6 +297,120 @@ namespace scripting::lua return convert(s, variable); }; + + game_type["getfunctions"] = [entity_type](const game&, const sol::this_state s, const std::string& filename) + { + if (scripting::script_function_table.find(filename) == scripting::script_function_table.end()) + { + throw std::runtime_error("File '" + filename + "' not found"); + } + + auto functions = sol::table::create(s.lua_state()); + + for (const auto& function : scripting::script_function_table[filename]) + { + functions[function.first] = sol::overload( + [filename, function](const entity& entity, const sol::this_state s, sol::variadic_args va) + { + std::vector arguments{}; + + for (auto arg : va) + { + arguments.push_back(convert({s, arg})); + } + + gsl::finally(&logfile::enable_vm_execute_hook); + logfile::disable_vm_execute_hook(); + + return convert(s, call_script_function(entity, filename, function.first, arguments)); + }, + [filename, function](const sol::this_state s, sol::variadic_args va) + { + std::vector arguments{}; + + for (auto arg : va) + { + arguments.push_back(convert({s, arg})); + } + + gsl::finally(&logfile::enable_vm_execute_hook); + logfile::disable_vm_execute_hook(); + + return convert(s, call_script_function(*::game::levelEntityId, filename, function.first, arguments)); + } + ); + } + + return functions; + }; + + game_type["scriptcall"] = [](const game&, const sol::this_state s, const std::string& filename, + const std::string function, sol::variadic_args va) + { + std::vector arguments{}; + + for (auto arg : va) + { + arguments.push_back(convert({s, arg})); + } + + gsl::finally(&logfile::enable_vm_execute_hook); + logfile::disable_vm_execute_hook(); + + return convert(s, call_script_function(*::game::levelEntityId, filename, function, arguments)); + }; + + game_type["detour"] = [](const game&, const sol::this_state s, const std::string& filename, + const std::string function_name, const sol::protected_function& function) + { + const auto pos = get_function_pos(filename, function_name); + logfile::vm_execute_hooks[pos] = function; + + auto detour = sol::table::create(function.lua_state()); + + detour["disable"] = [pos]() + { + logfile::vm_execute_hooks.erase(pos); + }; + + detour["enable"] = [pos, function]() + { + logfile::vm_execute_hooks[pos] = function; + }; + + detour["invoke"] = sol::overload( + [filename, function_name](const entity& entity, const sol::this_state s, sol::variadic_args va) + { + std::vector arguments{}; + + for (auto arg : va) + { + arguments.push_back(convert({s, arg})); + } + + gsl::finally(&logfile::enable_vm_execute_hook); + logfile::disable_vm_execute_hook(); + + return convert(s, call_script_function(entity, filename, function_name, arguments)); + }, + [filename, function_name](const sol::this_state s, sol::variadic_args va) + { + std::vector arguments{}; + + for (auto arg : va) + { + arguments.push_back(convert({s, arg})); + } + + gsl::finally(&logfile::enable_vm_execute_hook); + logfile::disable_vm_execute_hook(); + + return convert(s, call_script_function(*::game::levelEntityId, filename, function_name, arguments)); + } + ); + + return detour; + }; } } diff --git a/src/client/game/scripting/lua/value_conversion.cpp b/src/client/game/scripting/lua/value_conversion.cpp index 6d7d1b3..defbe14 100644 --- a/src/client/game/scripting/lua/value_conversion.cpp +++ b/src/client/game/scripting/lua/value_conversion.cpp @@ -2,6 +2,7 @@ #include "value_conversion.hpp" #include "../functions.hpp" #include "../execution.hpp" +#include ".../../component/logfile.hpp" namespace scripting::lua { @@ -100,7 +101,7 @@ namespace scripting::lua if (values.find(key) == values.end()) { - return sol::lua_value{}; + return sol::lua_value{s, sol::lua_nil}; } return convert(s, values.at(key).value); @@ -116,19 +117,46 @@ namespace scripting::lua return {state, table}; } + game::VariableValue convert_function(sol::lua_value value) + { + const auto function = value.as(); + const auto index = reinterpret_cast(logfile::vm_execute_hooks.size()); + + logfile::vm_execute_hooks[index] = function; + + game::VariableValue func; + func.type = game::SCRIPT_FUNCTION; + func.u.codePosValue = index; + + return func; + } + sol::lua_value convert_function(lua_State* state, const char* pos) { - return [pos](const entity& entity, const sol::this_state s, sol::variadic_args va) - { - std::vector arguments{}; - - for (auto arg : va) + return sol::overload( + [pos](const entity& entity, const sol::this_state s, sol::variadic_args va) { - arguments.push_back(convert({s, arg})); - } + std::vector arguments{}; - return convert(s, scripting::exec_ent_thread(entity, pos, arguments)); - }; + for (auto arg : va) + { + arguments.push_back(convert({s, arg})); + } + + return convert(s, exec_ent_thread(entity, pos, arguments)); + }, + [pos](const sol::this_state s, sol::variadic_args va) + { + std::vector arguments{}; + + for (auto arg : va) + { + arguments.push_back(convert({s, arg})); + } + + return convert(s, exec_ent_thread(*game::levelEntityId, pos, arguments)); + } + ); } } @@ -152,13 +180,7 @@ namespace scripting::lua } const auto variable_id = game::GetVariable(parent_id, id); - if (!variable_id) - { - return; - } - const auto variable = &game::scr_VarGlob->childVariableValue[variable_id + offset]; - const auto new_variable = convert({s, value}).get_raw(); game::AddRefToValue(new_variable.type, new_variable.u); @@ -177,13 +199,13 @@ namespace scripting::lua if (!id) { - return sol::lua_value{s}; + return sol::lua_value{s, sol::lua_nil}; } - const auto variable_id = game::GetVariable(parent_id, id); + const auto variable_id = game::FindVariable(parent_id, id); if (!variable_id) { - return sol::lua_value{s}; + return sol::lua_value{s, sol::lua_nil}; } const auto variable = game::scr_VarGlob->childVariableValue[variable_id + offset]; @@ -242,6 +264,11 @@ namespace scripting::lua return {value.as()}; } + if (value.is()) + { + return convert_function(value); + } + return {}; } @@ -287,6 +314,6 @@ namespace scripting::lua return {state, value.as()}; } - return {}; + return {state, sol::lua_nil}; } } diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 6a9ea61..60ebf8f 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -250,6 +250,7 @@ namespace game WEAK symbol scr_VarGlob{0x149B1D680, 0x148185F80}; WEAK symbol scr_VmPub{0x14A1938C0, 0x1487FC1C0}; + WEAK symbol scr_function_stack{0x14A19DE40, 0x148806740}; WEAK symbol command_whitelist{0x140808EF0, 0x1409B8DC0};