diff --git a/.gitmodules b/.gitmodules index d9af3664..14658e84 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "deps/stb"] path = deps/stb url = https://github.com/nothings/stb.git +[submodule "deps/asmjit"] + path = deps/asmjit + url = https://github.com/asmjit/asmjit.git diff --git a/deps/asmjit b/deps/asmjit new file mode 160000 index 00000000..0dd16b0a --- /dev/null +++ b/deps/asmjit @@ -0,0 +1 @@ +Subproject commit 0dd16b0a98ae1da48563c9cc62f757a9e6bbe9b6 diff --git a/deps/premake/asmjit.lua b/deps/premake/asmjit.lua new file mode 100644 index 00000000..ee932594 --- /dev/null +++ b/deps/premake/asmjit.lua @@ -0,0 +1,34 @@ +asmjit = { + source = path.join(dependencies.basePath, "asmjit"), +} + +function asmjit.import() + links { "asmjit" } + asmjit.includes() +end + +function asmjit.includes() + includedirs { + path.join(asmjit.source, "src") + } + + defines { + "ASMJIT_STATIC" + } +end + +function asmjit.project() + project "asmjit" + language "C++" + + asmjit.includes() + + files { + path.join(asmjit.source, "src/**.cpp"), + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, asmjit) diff --git a/src/component/notifies.cpp b/src/component/notifies.cpp new file mode 100644 index 00000000..36651638 --- /dev/null +++ b/src/component/notifies.cpp @@ -0,0 +1,119 @@ +#include +#include "loader/component_loader.hpp" +#include "scheduler.hpp" + +#include "game/scripting/entity.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/lua/value_conversion.hpp" +#include "game/scripting/lua/error.hpp" +#include "notifies.hpp" + +#include + +namespace notifies +{ + std::unordered_map vm_execute_hooks; + bool hook_enabled = true; + + namespace + { + char empty_function[2] = {0x32, 0x34}; // CHECK_CLEAR_PARAMS, END + + 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()) + { + return false; + } + + if (!hook_enabled && pos > (char*)vm_execute_hooks.size()) + { + hook_enabled = true; + return false; + } + + const auto hook = vm_execute_hooks[pos]; + const auto state = hook.lua_state(); + + const auto self_id = local_id_to_entity(game::scr_VmPub->function_frame->fs.localId); + const auto self = scripting::entity(self_id); + + 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); + + const auto value = scripting::lua::convert({ state, result }); + const auto type = value.get_raw().type; + + game::Scr_ClearOutParams(); + + if (result.valid() && type && type < game::SCRIPT_END) + { + scripting::push_value(value); + } + + 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.mov(dword_ptr(rbp, 0xA4), r15d); + + a.jmp(game::base_address + 0x5C90B3); + + a.bind(replace); + + a.popad64(); + a.mov(r14, (char*)empty_function); + a.jmp(end); + } + } + + void clear_callbacks() + { + vm_execute_hooks.clear(); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + utils::hook::jump(game::base_address + 0x5C90A5, utils::hook::assemble(vm_execute_stub), true); + } + }; +} + +REGISTER_COMPONENT(notifies::component) \ No newline at end of file diff --git a/src/component/notifies.hpp b/src/component/notifies.hpp new file mode 100644 index 00000000..e7ab5a6c --- /dev/null +++ b/src/component/notifies.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace notifies +{ + extern std::unordered_map vm_execute_hooks; + extern bool hook_enabled; + + void clear_callbacks(); +} \ No newline at end of file diff --git a/src/component/scripting.cpp b/src/component/scripting.cpp index 362c3246..6248ba53 100644 --- a/src/component/scripting.cpp +++ b/src/component/scripting.cpp @@ -4,9 +4,10 @@ #include "game/game.hpp" #include "scheduler.hpp" +#include "scripting.hpp" #include "game/scripting/event.hpp" -#include "game/scripting/execution.hpp" +#include "game/scripting/functions.hpp" #include "game/scripting/lua/engine.hpp" #include @@ -14,6 +15,7 @@ namespace scripting { std::unordered_map> fields_table; + std::unordered_map> script_function_table; namespace { @@ -24,6 +26,11 @@ namespace scripting utils::hook::detour scr_add_class_field_hook; + utils::hook::detour scr_set_thread_position_hook; + utils::hook::detour process_script_hook; + + std::string current_file; + bool running = false; void vm_notify_stub(const unsigned int notify_list_owner_id, const game::scr_string_t string_value, @@ -78,6 +85,28 @@ namespace scripting scr_add_class_field_hook.invoke(classnum, _name, canonicalString, offset); } + + void process_script_stub(const char* filename) + { + current_file = filename; + + const auto file_id = atoi(filename); + if (file_id) + { + current_file = scripting::find_token(file_id); + } + + 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 @@ -91,6 +120,8 @@ namespace scripting g_shutdown_game_hook.create(game::base_address + 0x4CBAD0, g_shutdown_game_stub); scr_add_class_field_hook.create(game::base_address + 0x5C2C30, scr_add_class_field_stub); + scr_set_thread_position_hook.create(game::base_address + 0x5BC7E0, scr_set_thread_position_stub); + process_script_hook.create(game::base_address + 0x5C6160, process_script_stub); scheduler::loop([]() { diff --git a/src/component/scripting.hpp b/src/component/scripting.hpp index adf9be6c..1f16a663 100644 --- a/src/component/scripting.hpp +++ b/src/component/scripting.hpp @@ -3,4 +3,5 @@ namespace scripting { extern std::unordered_map> fields_table; + extern std::unordered_map> script_function_table; } \ No newline at end of file diff --git a/src/game/scripting/execution.cpp b/src/game/scripting/execution.cpp index da67f708..f3580b8c 100644 --- a/src/game/scripting/execution.cpp +++ b/src/game/scripting/execution.cpp @@ -16,14 +16,6 @@ namespace scripting return value_ptr; } - void push_value(const script_value& value) - { - auto* value_ptr = allocate_argument(); - *value_ptr = value.get_raw(); - - game::AddRefToValue(value_ptr->type, value_ptr->u); - } - int get_field_id(const int classnum, const std::string& field) { if (scripting::fields_table[classnum].find(field) != scripting::fields_table[classnum].end()) @@ -49,6 +41,14 @@ namespace scripting } } + void push_value(const script_value& value) + { + auto* value_ptr = allocate_argument(); + *value_ptr = value.get_raw(); + + game::AddRefToValue(value_ptr->type, value_ptr->u); + } + void notify(const entity& entity, const std::string& event, const std::vector& arguments) { stack_isolation _; @@ -122,6 +122,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/game/scripting/execution.hpp b/src/game/scripting/execution.hpp index 2780e271..03f0f652 100644 --- a/src/game/scripting/execution.hpp +++ b/src/game/scripting/execution.hpp @@ -5,6 +5,8 @@ namespace scripting { + void push_value(const script_value& value); + script_value call_function(const std::string& name, const std::vector& arguments); script_value call_function(const std::string& name, const entity& entity, const std::vector& arguments); @@ -22,6 +24,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/game/scripting/function_tables.cpp b/src/game/scripting/function_tables.cpp index cfbcb41b..dc155550 100644 --- a/src/game/scripting/function_tables.cpp +++ b/src/game/scripting/function_tables.cpp @@ -12,10 +12,10 @@ namespace scripting {"nullsub_428", 0x3}, {"sub_502950", 0x4}, {"sub_504f40", 0x5}, - {"meleeapplyinitialvelocity", 0x6}, - {"sub_508680", 0x7}, - {"sub_5086a0", 0x8}, - {"sub_5087a0", 0x9}, + {"setphysicsgravitydir", 0x6}, + {"gettimescale", 0x7}, + {"settimescale", 0x8}, + {"setslowmotionview", 0x9}, {"sub_5086c0", 0xA}, {"sub_5085a0", 0xB}, {"sub_504fd0", 0xC}, @@ -125,11 +125,11 @@ namespace scripting {"setblur", 0x76}, {"musicplay", 0x77}, {"musicstop", 0x78}, - {"sub_507890", 0x79}, - {"setteammode", 0x7A}, - {"sub_4f6990", 0x7B}, - {"sub_4f6a20", 0x7C}, - {"sub_4f6c90", 0x7D}, + {"soundfade", 0x79}, + {"soundsettimescalefactor", 0x7A}, + {"soundresettimescale", 0x7B}, + {"setocclusionpreset", 0x7C}, + {"levelsoundfade", 0x7D}, {"sub_4f6da0", 0x7E}, {"sub_4f6dd0", 0x7F}, {"sub_507a70", 0x80}, @@ -223,8 +223,8 @@ namespace scripting {"sub_504c60", 0xDB}, {"sub_505030", 0xDC}, {"sub_5050a0", 0xDD}, - {"sub_505360", 0xDE}, - {"sub_505520", 0xDF}, + {"getaiarray", 0xDE}, + {"getaispeciesarray", 0xDF}, {"getspawnerarray", 0xE0}, {"getcorpsearray", 0xE1}, {"getspawnerteamarray", 0xE2}, @@ -874,12 +874,12 @@ namespace scripting {"allowprone", 0x8123}, {"allowlean", 0x8124}, {"allowswim", 0x8125}, - {"sub_4bb590", 0x8126}, - {"sub_4bb320", 0x8128}, - {"sub_4bb3a0", 0x8129}, - {"sub_4bb730", 0x812A}, - {"sub_4bb780", 0x812F}, - {"sub_4bb3c0", 0x8130}, + {"setocclusion", 0x8126}, + {"deactivateocclusion", 0x8128}, + {"deactivateallocclusion", 0x8129}, + {"isocclusionenabled", 0x812A}, + {"setreverbfromtable", 0x812F}, + {"setvolmodfromtable", 0x8130}, {"sub_4bb7d0", 0x8131}, {"sub_4bb930", 0x8132}, {"sub_4bb240", 0x8133}, @@ -1772,7 +1772,13 @@ namespace scripting std::unordered_map token_map = { + {"main", 616}, {"player", 794}, {"default_start", 10126}, }; + + std::unordered_map file_list = + { + + }; } diff --git a/src/game/scripting/functions.cpp b/src/game/scripting/functions.cpp index 56f3b8e8..ced9d6bc 100644 --- a/src/game/scripting/functions.cpp +++ b/src/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/game/scripting/functions.hpp b/src/game/scripting/functions.hpp index 2227753b..b788b500 100644 --- a/src/game/scripting/functions.hpp +++ b/src/game/scripting/functions.hpp @@ -6,9 +6,11 @@ namespace scripting extern std::unordered_map method_map; extern std::unordered_map function_map; extern std::unordered_map token_map; + extern std::unordered_map file_list; 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/game/scripting/lua/context.cpp b/src/game/scripting/lua/context.cpp index 8f7efc7f..947d2f70 100644 --- a/src/game/scripting/lua/context.cpp +++ b/src/game/scripting/lua/context.cpp @@ -6,6 +6,8 @@ #include "../execution.hpp" #include "../functions.hpp" +#include "../../../component/notifies.hpp" +#include "../../../component/scripting.hpp" #include "../../../component/command.hpp" #include "../../../component/chat.hpp" @@ -373,6 +375,74 @@ namespace scripting::lua { chat::print(msg); }; + + 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); + notifies::vm_execute_hooks[pos] = function; + + auto detour = sol::table::create(function.lua_state()); + + detour["disable"] = [pos]() + { + notifies::vm_execute_hooks.erase(pos); + }; + + detour["enable"] = [pos, function]() + { + notifies::vm_execute_hooks[pos] = function; + }; + + detour["invoke"] = [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})); + } + + notifies::hook_enabled = false; + const auto result = convert(s, call_script_function(entity, filename, function_name, arguments)); + notifies::hook_enabled = true; + + return result; + }; + + return detour; + }; + + 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] = [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})); + } + + notifies::hook_enabled = false; + const auto result = convert(s, call_script_function(entity, filename, function.first, arguments)); + notifies::hook_enabled = true; + + return result; + }; + } + + return functions; + }; } } diff --git a/src/game/scripting/lua/engine.cpp b/src/game/scripting/lua/engine.cpp index 983f823c..9e23d415 100644 --- a/src/game/scripting/lua/engine.cpp +++ b/src/game/scripting/lua/engine.cpp @@ -2,6 +2,7 @@ #include "engine.hpp" #include "context.hpp" +#include "../../../component/notifies.hpp" #include "../execution.hpp" #include @@ -46,6 +47,7 @@ namespace scripting::lua::engine void stop() { + notifies::clear_callbacks(); get_scripts().clear(); } diff --git a/src/game/scripting/lua/value_conversion.cpp b/src/game/scripting/lua/value_conversion.cpp index 3cfd31c9..c6ba8546 100644 --- a/src/game/scripting/lua/value_conversion.cpp +++ b/src/game/scripting/lua/value_conversion.cpp @@ -2,6 +2,7 @@ #include "value_conversion.hpp" #include "../functions.hpp" #include "../execution.hpp" +#include ".../../component/notifies.hpp" namespace scripting::lua { @@ -140,6 +141,20 @@ namespace scripting::lua return script_value(variable); } + game::VariableValue convert_function(sol::lua_value value) + { + const auto function = value.as(); + const auto index = (char*)notifies::vm_execute_hooks.size() + 1; + + notifies::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) diff --git a/src/game/symbols.hpp b/src/game/symbols.hpp index 361dd0bb..c1978da2 100644 --- a/src/game/symbols.hpp +++ b/src/game/symbols.hpp @@ -127,4 +127,5 @@ namespace game WEAK symbol scr_VarGlob{0xB617C00}; WEAK symbol scr_VmPub{0xBA9EE40}; + WEAK symbol scr_function_stack{0xBAA93C0}; } \ No newline at end of file diff --git a/src/stdinc.hpp b/src/stdinc.hpp index 700d96ad..8d08152a 100644 --- a/src/stdinc.hpp +++ b/src/stdinc.hpp @@ -24,6 +24,7 @@ #include #include #include +#include #ifdef max #undef max @@ -36,4 +37,7 @@ #include #include +#include +#include + using namespace std::literals; \ No newline at end of file diff --git a/src/utils/hook.cpp b/src/utils/hook.cpp index 599879cc..a5a2bb31 100644 --- a/src/utils/hook.cpp +++ b/src/utils/hook.cpp @@ -25,6 +25,76 @@ namespace utils::hook } __; } + void assembler::pushad64() + { + this->push(rax); + this->push(rcx); + this->push(rdx); + this->push(rbx); + this->push(rsp); + this->push(rbp); + this->push(rsi); + this->push(rdi); + + this->sub(rsp, 0x40); + } + + void assembler::popad64() + { + this->add(rsp, 0x40); + + this->pop(rdi); + this->pop(rsi); + this->pop(rbp); + this->pop(rsp); + this->pop(rbx); + this->pop(rdx); + this->pop(rcx); + this->pop(rax); + } + + void assembler::prepare_stack_for_call() + { + const auto reserve_callee_space = this->newLabel(); + const auto stack_unaligned = this->newLabel(); + + this->test(rsp, 0xF); + this->jnz(stack_unaligned); + + this->sub(rsp, 0x8); + this->push(rsp); + + this->push(rax); + this->mov(rax, ptr(rsp, 8, 8)); + this->add(rax, 0x8); + this->mov(ptr(rsp, 8, 8), rax); + this->pop(rax); + + this->jmp(reserve_callee_space); + + this->bind(stack_unaligned); + this->push(rsp); + + this->bind(reserve_callee_space); + this->sub(rsp, 0x40); + } + + void assembler::restore_stack_after_call() + { + this->lea(rsp, ptr(rsp, 0x40)); + this->pop(rsp); + } + + asmjit::Error assembler::call(void* target) + { + return Assembler::call(size_t(target)); + } + + asmjit::Error assembler::jmp(void* target) + { + return Assembler::jmp(size_t(target)); + } + detour::detour(const size_t place, void* target) : detour(reinterpret_cast(place), target) { } @@ -115,7 +185,7 @@ namespace utils::hook copy(reinterpret_cast(place), data, length); } - bool is_relatively_far(const void* pointer, const void* data, int offset) + bool is_relatively_far(const void* pointer, const void* data, const int offset) { const int64_t diff = size_t(data) - (size_t(pointer) + offset); const auto small_diff = int32_t(diff); @@ -178,4 +248,47 @@ namespace utils::hook { return jump(pointer, reinterpret_cast(data), use_far); } + + void* assemble(const std::function& asm_function) + { + static asmjit::JitRuntime runtime; + + asmjit::CodeHolder code; + code.init(runtime.environment()); + + assembler a(&code); + + asm_function(a); + + void* result = nullptr; + runtime.add(&result, &code); + + return result; + } + + void inject(void* pointer, const void* data) + { + if (is_relatively_far(pointer, data, 4)) + { + throw std::runtime_error("Too far away to create 32bit relative branch"); + } + + set(pointer, int32_t(size_t(data) - (size_t(pointer) + 4))); + } + + void inject(const size_t pointer, const void* data) + { + return inject(reinterpret_cast(pointer), data); + } + + void* follow_branch(void* address) + { + auto* const data = static_cast(address); + if (*data != 0xE8 && *data != 0xE9) + { + throw std::runtime_error("No branch instruction found"); + } + + return extract(data + 1); + } } diff --git a/src/utils/hook.hpp b/src/utils/hook.hpp index 90977996..e08df944 100644 --- a/src/utils/hook.hpp +++ b/src/utils/hook.hpp @@ -1,7 +1,36 @@ #pragma once +#include +#include + +using namespace asmjit::x86; namespace utils::hook { + class assembler : public Assembler + { + public: + using Assembler::Assembler; + using Assembler::call; + using Assembler::jmp; + + void pushad64(); + void popad64(); + + void prepare_stack_for_call(); + void restore_stack_after_call(); + + template + void call_aligned(T&& target) + { + this->prepare_stack_for_call(); + this->call(std::forward(target)); + this->restore_stack_after_call(); + } + + asmjit::Error call(void* target); + asmjit::Error jmp(void* target); + }; + class detour { public: @@ -15,7 +44,7 @@ namespace utils::hook this->operator=(std::move(other)); } - detour& operator= (detour&& other) noexcept + detour& operator=(detour&& other) noexcept { if (this != &other) { @@ -32,7 +61,7 @@ namespace utils::hook } detour(const detour&) = delete; - detour& operator= (const detour&) = delete; + detour& operator=(const detour&) = delete; void enable() const; void disable() const; @@ -47,8 +76,8 @@ namespace utils::hook return static_cast(this->get_original()); } - template - T invoke(Args... args) + template + T invoke(Args ... args) { return static_cast(this->get_original())(args...); } @@ -76,15 +105,17 @@ namespace utils::hook void jump(size_t pointer, void* data, bool use_far = false); void jump(size_t pointer, size_t data, bool use_far = false); + void* assemble(const std::function& asm_function); + void inject(void* pointer, const void* data); void inject(size_t pointer, const void* data); template T extract(void* address) { - const auto data = static_cast(address); + auto* const data = static_cast(address); const auto offset = *reinterpret_cast(data); - return reinterpret_cast(data + offset + 4); + return reinterpret_cast(data + offset + 4); } void* follow_branch(void* address); @@ -108,14 +139,14 @@ namespace utils::hook } template - static T invoke(size_t func, Args... args) + static T invoke(size_t func, Args ... args) { return reinterpret_cast(func)(args...); } template - static T invoke(void* func, Args... args) + static T invoke(void* func, Args ... args) { return static_cast(func)(args...); } -} \ No newline at end of file +}