Support calling & hooking functions in GSC scripts

This commit is contained in:
Federico Cecchetto 2021-12-10 00:20:40 +01:00
parent f6cf444e07
commit b0a25ec0df
12 changed files with 374 additions and 21 deletions

View File

@ -13,6 +13,8 @@
namespace logfile
{
std::unordered_map<const char*, sol::protected_function> vm_execute_hooks;
namespace
{
utils::hook::detour scr_player_killed_hook;
@ -21,6 +23,10 @@ namespace logfile
std::vector<sol::protected_function> player_killed_callbacks;
std::vector<sol::protected_function> 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<void(*)(int)>(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<char*>(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<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);
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<char*>(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);
}
};
}

View File

@ -2,7 +2,12 @@
namespace logfile
{
extern std::unordered_map<const char*, sol::protected_function> 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();
}

View File

@ -5,20 +5,29 @@
#include <utils/hook.hpp>
#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<std::string, std::unordered_map<std::string, const char*>> 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<void>(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<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
@ -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();

View File

@ -0,0 +1,6 @@
#pragma once
namespace scripting
{
extern std::unordered_map<std::string, std::unordered_map<std::string, const char*>> script_function_table;
}

View File

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

@ -22,6 +22,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

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

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

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

View File

@ -8,6 +8,7 @@
#include "../../../component/command.hpp"
#include "../../../component/logfile.hpp"
#include "../../../component/scripting.hpp"
#include <utils/string.hpp>
@ -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<script_value> 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<unsigned int*>(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<script_value> 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<script_value> 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<script_value> 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<script_value> 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<script_value> 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;
};
}
}

View File

@ -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<sol::protected_function>();
const auto index = reinterpret_cast<char*>(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<script_value> 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<script_value> 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<script_value> 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<vector>()};
}
if (value.is<sol::protected_function>())
{
return convert_function(value);
}
return {};
}
@ -287,6 +314,6 @@ namespace scripting::lua
return {state, value.as<vector>()};
}
return {};
return {state, sol::lua_nil};
}
}

View File

@ -250,6 +250,7 @@ namespace game
WEAK symbol<scrVarGlob_t> scr_VarGlob{0x149B1D680, 0x148185F80};
WEAK symbol<scrVmPub_t> scr_VmPub{0x14A1938C0, 0x1487FC1C0};
WEAK symbol<function_stack_t> scr_function_stack{0x14A19DE40, 0x148806740};
WEAK symbol<const char*> command_whitelist{0x140808EF0, 0x1409B8DC0};