Support calling & detouring gsc script functions

This commit is contained in:
Federico Cecchetto 2021-08-30 04:58:10 +02:00
parent e3102651b2
commit 3d1e262c16
19 changed files with 519 additions and 36 deletions

3
.gitmodules vendored
View File

@ -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

1
deps/asmjit vendored Submodule

@ -0,0 +1 @@
Subproject commit 0dd16b0a98ae1da48563c9cc62f757a9e6bbe9b6

34
deps/premake/asmjit.lua vendored Normal file
View File

@ -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)

119
src/component/notifies.cpp Normal file
View File

@ -0,0 +1,119 @@
#include <stdinc.hpp>
#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 <utils/hook.hpp>
namespace notifies
{
std::unordered_map<const char*, sol::protected_function> 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<sol::lua_value> 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)

View File

@ -0,0 +1,9 @@
#pragma once
namespace notifies
{
extern std::unordered_map<const char*, sol::protected_function> vm_execute_hooks;
extern bool hook_enabled;
void clear_callbacks();
}

View File

@ -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 <utils/hook.hpp>
@ -14,6 +15,7 @@
namespace scripting
{
std::unordered_map<int, std::unordered_map<std::string, int>> fields_table;
std::unordered_map<std::string, std::unordered_map<std::string, const char*>> 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<void>(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<void>(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<void>(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([]()
{

View File

@ -3,4 +3,5 @@
namespace scripting
{
extern std::unordered_map<int, std::unordered_map<std::string, int>> fields_table;
extern std::unordered_map<std::string, std::unordered_map<std::string, const char*>> script_function_table;
}

View File

@ -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<script_value>& 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<script_value>& arguments)
{
const auto pos = get_function_pos(filename, function);
return exec_ent_thread(entity, pos, arguments);
}
static std::unordered_map<unsigned int, std::unordered_map<std::string, script_value>> custom_fields;
script_value get_custom_field(const entity& entity, const std::string& field)

View File

@ -5,6 +5,8 @@
namespace scripting
{
void push_value(const script_value& value);
script_value call_function(const std::string& name, const std::vector<script_value>& arguments);
script_value call_function(const std::string& name, const entity& entity,
const std::vector<script_value>& arguments);
@ -22,6 +24,9 @@ namespace scripting
}
script_value exec_ent_thread(const entity& entity, const char* pos, const std::vector<script_value>& 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<script_value>& arguments);
void clear_entity_fields(const entity& entity);
void clear_custom_fields();

View File

@ -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<std::string, unsigned> token_map =
{
{"main", 616},
{"player", 794},
{"default_start", 10126},
};
std::unordered_map<unsigned, std::string> file_list =
{
};
}

View File

@ -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);

View File

@ -6,9 +6,11 @@ namespace scripting
extern std::unordered_map<std::string, unsigned> method_map;
extern std::unordered_map<std::string, unsigned> function_map;
extern std::unordered_map<std::string, unsigned> token_map;
extern std::unordered_map<unsigned, std::string> 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);
}

View File

@ -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<script_value> 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<script_value> 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;
};
}
}

View File

@ -2,6 +2,7 @@
#include "engine.hpp"
#include "context.hpp"
#include "../../../component/notifies.hpp"
#include "../execution.hpp"
#include <utils/io.hpp>
@ -46,6 +47,7 @@ namespace scripting::lua::engine
void stop()
{
notifies::clear_callbacks();
get_scripts().clear();
}

View File

@ -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<sol::protected_function>();
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)

View File

@ -127,4 +127,5 @@ namespace game
WEAK symbol<scrVarGlob_t> scr_VarGlob{0xB617C00};
WEAK symbol<scrVmPub_t> scr_VmPub{0xBA9EE40};
WEAK symbol<function_stack_t> scr_function_stack{0xBAA93C0};
}

View File

@ -24,6 +24,7 @@
#include <deque>
#include <optional>
#include <map>
#include <stdexcept>
#ifdef max
#undef max
@ -36,4 +37,7 @@
#include <MinHook.h>
#include <gsl/gsl>
#include <asmjit/core/jitruntime.h>
#include <asmjit/x86/x86assembler.h>
using namespace std::literals;

View File

@ -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<void*>(place), target)
{
}
@ -115,7 +185,7 @@ namespace utils::hook
copy(reinterpret_cast<void*>(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<void*>(data), use_far);
}
void* assemble(const std::function<void(assembler&)>& 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<int32_t>(pointer, int32_t(size_t(data) - (size_t(pointer) + 4)));
}
void inject(const size_t pointer, const void* data)
{
return inject(reinterpret_cast<void*>(pointer), data);
}
void* follow_branch(void* address)
{
auto* const data = static_cast<uint8_t*>(address);
if (*data != 0xE8 && *data != 0xE9)
{
throw std::runtime_error("No branch instruction found");
}
return extract<void*>(data + 1);
}
}

View File

@ -1,7 +1,36 @@
#pragma once
#include <asmjit/core/jitruntime.h>
#include <asmjit/x86/x86assembler.h>
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 <typename T>
void call_aligned(T&& target)
{
this->prepare_stack_for_call();
this->call(std::forward<T>(target));
this->restore_stack_after_call();
}
asmjit::Error call(void* target);
asmjit::Error jmp(void* target);
};
class detour
{
public:
@ -47,7 +76,7 @@ namespace utils::hook
return static_cast<T*>(this->get_original());
}
template <typename T, typename... Args>
template <typename T = void, typename... Args>
T invoke(Args ... args)
{
return static_cast<T(*)(Args ...)>(this->get_original())(args...);
@ -76,13 +105,15 @@ 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<void(assembler&)>& asm_function);
void inject(void* pointer, const void* data);
void inject(size_t pointer, const void* data);
template <typename T>
T extract(void* address)
{
const auto data = static_cast<uint8_t*>(address);
auto* const data = static_cast<uint8_t*>(address);
const auto offset = *reinterpret_cast<int32_t*>(data);
return reinterpret_cast<T>(data + offset + 4);
}