better GSC api (#310)

This commit is contained in:
m 2022-10-14 11:39:27 -05:00 committed by GitHub
parent dd007b12db
commit 59718fc9d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 290 additions and 117 deletions

View File

@ -609,10 +609,12 @@ namespace command
add_commands_generic();
gsc::function::add("executecommand", []()
gsc::function::add("executecommand", [](const gsc::function_args& args)
{
const auto cmd = gsc::get_argument(0).as<std::string>();
const auto cmd = args[0].as<std::string>();
command::execute(cmd, true);
return scripting::script_value{};
});
}

View File

@ -225,9 +225,9 @@ namespace dedicated
}), true);
// return 0 so the game doesn't override the cfg
gsc::function::add("isusingmatchrulesdata", []()
gsc::function::add("isusingmatchrulesdata", [](const gsc::function_args& args)
{
game::Scr_AddInt(0);
return 0;
});
// delay console commands until the initialization is done // COULDN'T FOUND

View File

@ -26,11 +26,13 @@
namespace gsc
{
void* func_table[0x1000]{};
void* meth_table[0x1000]{};
builtin_function func_table[0x1000]{};
builtin_method meth_table[0x1000]{};
namespace
{
utils::hook::detour scr_emit_function_hook;
game::dvar_t* developer_script = nullptr;
auto compiler = ::gsc::compiler();
@ -42,8 +44,8 @@ namespace gsc
std::unordered_map<std::string, unsigned int> init_handles;
std::unordered_map<std::string, game::ScriptFile*> loaded_scripts;
std::unordered_map<builtin_function, unsigned int> functions;
std::unordered_map<builtin_method, unsigned int> methods;
std::unordered_map<unsigned int, script_function> functions;
std::unordered_map<unsigned int, script_method> methods;
std::optional<std::string> gsc_error;
@ -52,6 +54,8 @@ namespace gsc
auto function_id_start = 0x30A;
auto method_id_start = 0x8586;
game::scr_entref_t saved_ent_ref;
std::string unknown_function_error{};
unsigned int current_filename{};
@ -349,11 +353,16 @@ namespace gsc
}
}
void builtin_call_error(const std::string& error_str)
std::uint16_t get_function_id()
{
const auto pos = game::scr_function_stack->pos;
const auto function_id = *reinterpret_cast<std::uint16_t*>(
return *reinterpret_cast<std::uint16_t*>(
reinterpret_cast<size_t>(pos - 2));
}
void builtin_call_error(const std::string& error_str)
{
const auto function_id = get_function_id();
if (function_id > 0x1000)
{
@ -367,11 +376,12 @@ namespace gsc
}
}
void* vm_error_stub(void* a1)
void vm_error_stub(void* a1)
{
if (!developer_script->current.enabled && !force_error_print)
{
return utils::hook::invoke<void*>(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), a1);
utils::hook::invoke<void>(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), a1);
return;
}
console::warn("*********** script runtime error *************\n");
@ -405,7 +415,7 @@ namespace gsc
print_callstack();
console::warn("**********************************************\n");
return utils::hook::invoke<void*>(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), a1);
utils::hook::invoke<void*>(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), a1);
}
void get_unknown_function_error(const char* code_pos)
@ -471,31 +481,49 @@ namespace gsc
return res;
}
void register_gsc_functions_stub(void* a1, void* a2)
void scr_emit_function_stub(unsigned int filename, unsigned int thread_name, char* code_pos)
{
utils::hook::invoke<void>(SELECT_VALUE(0x2E0F50_b, 0x1CE010_b), a1, a2);
for (const auto& function : functions)
{
game::Scr_RegisterFunction(function.first, 0, function.second);
}
current_filename = filename;
scr_emit_function_hook.invoke<void>(filename, thread_name, code_pos);
}
void register_gsc_methods_stub(void* a1, void* a2)
function_args get_arguments()
{
utils::hook::invoke<void>(SELECT_VALUE(0x2E0FB0_b, 0x1CE120_b), a1, a2);
for (const auto& method : methods)
std::vector<scripting::script_value> args;
for (auto i = 0; static_cast<unsigned int>(i) < game::scr_VmPub->outparamcount; ++i)
{
game::Scr_RegisterFunction(method.first, 0, method.second);
const auto value = game::scr_VmPub->top[-i];
args.push_back(value);
}
return args;
}
void execute_custom_function(builtin_function function)
void return_value(const scripting::script_value& value)
{
if (game::scr_VmPub->outparamcount)
{
game::Scr_ClearOutParams();
}
scripting::push_value(value);
}
void call_custom_function(const uint16_t id)
{
auto error = false;
try
{
function();
const auto& function = functions[id];
const auto result = function(get_arguments());
const auto type = result.get_raw().type;
if (type)
{
return_value(result);
}
}
catch (const std::exception& e)
{
@ -510,13 +538,20 @@ namespace gsc
}
}
void execute_custom_method(scripting::script_function method, game::scr_entref_t ent_ref)
void call_custom_method(const uint16_t id)
{
auto error = false;
try
{
method(ent_ref);
const auto& method = methods[id];
const auto result = method(saved_ent_ref, get_arguments());
const auto type = result.get_raw().type;
if (type)
{
return_value(result);
}
}
catch (const std::exception& e)
{
@ -533,10 +568,8 @@ namespace gsc
void vm_call_builtin_function_stub(builtin_function function)
{
auto is_custom_function = false;
{
is_custom_function = functions.find(function) != functions.end();
}
const auto function_id = get_function_id();
const auto is_custom_function = functions.find(function_id) != functions.end();
if (!is_custom_function)
{
@ -544,15 +577,30 @@ namespace gsc
}
else
{
execute_custom_function(function);
call_custom_function(function_id);
}
}
utils::hook::detour scr_emit_function_hook;
void scr_emit_function_stub(unsigned int filename, unsigned int thread_name, char* code_pos)
game::scr_entref_t get_entity_id_stub(unsigned int ent_id)
{
current_filename = filename;
scr_emit_function_hook.invoke<void>(filename, thread_name, code_pos);
const auto ref = game::Scr_GetEntityIdRef(ent_id);
saved_ent_ref = ref;
return ref;
}
void vm_call_builtin_method_stub(builtin_method method)
{
const auto function_id = get_function_id();
const auto is_custom_function = methods.find(function_id) != methods.end();
if (!is_custom_function)
{
method(saved_ent_ref);
}
else
{
call_custom_method(function_id);
}
}
}
@ -597,52 +645,67 @@ namespace gsc
}
}
scripting::script_value get_argument(int index)
{
if (index >= static_cast<int>(game::scr_VmPub->outparamcount))
{
return {};
}
return game::scr_VmPub->top[-index];
}
namespace function
{
void add(const std::string& name, builtin_function function)
void add(const std::string& name, script_function function)
{
if (xsk::gsc::h1::resolver::find_function(name))
{
const auto id = xsk::gsc::h1::resolver::function_id(name);
functions[function] = id;
functions[id] = function;
}
else
{
const auto id = ++function_id_start;
xsk::gsc::h1::resolver::add_function(name, static_cast<std::uint16_t>(id));
functions[function] = id;
functions[id] = function;
}
}
}
namespace method
{
void add(const std::string& name, builtin_method method)
void add(const std::string& name, script_method method)
{
if (xsk::gsc::h1::resolver::find_method(name))
{
const auto id = xsk::gsc::h1::resolver::method_id(name);
methods[method] = id;
methods[id] = method;
}
else
{
const auto id = ++method_id_start;
xsk::gsc::h1::resolver::add_method(name, static_cast<std::uint16_t>(id));
methods[method] = id;
methods[id] = method;
}
}
}
function_args::function_args(std::vector<scripting::script_value> values)
: values_(values)
{
}
unsigned int function_args::size() const
{
return static_cast<unsigned int>(this->values_.size());
}
std::vector<scripting::script_value> function_args::get_raw() const
{
return this->values_;
}
scripting::value_wrap function_args::get(const int index) const
{
if (index >= this->values_.size())
{
throw std::runtime_error(utils::string::va("parameter %d does not exist", index));
}
return { this->values_[index], index };
}
class component final : public component_interface
{
public:
@ -693,9 +756,6 @@ namespace gsc
utils::hook::call(SELECT_VALUE(0x3BD75A_b, 0x50473A_b), find_variable_stub);
scr_emit_function_hook.create(SELECT_VALUE(0x3BD680_b, 0x504660_b), scr_emit_function_stub);
utils::hook::call(SELECT_VALUE(0x3BDC4F_b, 0x504C7F_b), register_gsc_functions_stub);
utils::hook::call(SELECT_VALUE(0x3BDC5B_b, 0x504C8B_b), register_gsc_methods_stub);
utils::hook::set<uint32_t>(SELECT_VALUE(0x3BD86C_b, 0x50484C_b), 0x1000); // change builtin func count
utils::hook::set<uint32_t>(SELECT_VALUE(0x3BD872_b, 0x504852_b) + 4,
@ -712,47 +772,62 @@ namespace gsc
utils::hook::inject(SELECT_VALUE(0x3BDC36_b, 0x504C66_b) + 3, &meth_table);
utils::hook::set<uint32_t>(SELECT_VALUE(0x3BDC3F_b, 0x504C6F_b), sizeof(meth_table));
/*
trying to do a jump hook to push the ent reference (if it exists) and the builtin function/methods works, but
if longjmp is called because of a runtime error in our custom functions/methods, then the game just kinda dies
or gets incredibly slow but will eventually load. for functions, the workaround is easy but for methods, we still
have to remember the entity that called the method so the workaround is just hooking the Scr_GetEntityIdRef call
*/
utils::hook::nop(SELECT_VALUE(0x3CB723_b, 0x512783_b), 8);
utils::hook::call(SELECT_VALUE(0x3CB723_b, 0x512783_b), vm_call_builtin_function_stub);
function::add("print", []()
utils::hook::call(SELECT_VALUE(0x3CBA12_b, 0x512A72_b), get_entity_id_stub);
utils::hook::nop(SELECT_VALUE(0x3CBA46_b, 0x512AA6_b), 6);
utils::hook::nop(SELECT_VALUE(0x3CBA4E_b, 0x512AAE_b), 2);
utils::hook::call(SELECT_VALUE(0x3CBA46_b, 0x512AA6_b), vm_call_builtin_method_stub);
function::add("print", [](const function_args& args)
{
const auto num = game::Scr_GetNumParam();
std::string buffer{};
for (auto i = 0; i < num; i++)
for (auto i = 0u; i < args.size(); ++i)
{
const auto str = game::Scr_GetString(i);
const auto str = args[i].as<std::string>();
buffer.append(str);
buffer.append("\t");
}
console::info("%s\n", buffer.data());
return scripting::script_value{};
});
function::add("assert", []()
function::add("assert", [](const function_args& args)
{
const auto expr = get_argument(0).as<int>();
const auto expr = args[0].as<int>();
if (!expr)
{
throw std::runtime_error("assert fail");
}
return scripting::script_value{};
});
function::add("assertex", []()
function::add("assertex", [](const function_args& args)
{
const auto expr = get_argument(0).as<int>();
const auto expr = args[0].as<int>();
if (!expr)
{
const auto error = get_argument(1).as<std::string>();
const auto error = args[1].as<std::string>();
throw std::runtime_error(error);
}
return scripting::script_value{};
});
function::add("replacefunc", []()
function::add("replacefunc", [](const function_args& args)
{
const auto what = get_argument(0).get_raw();
const auto with = get_argument(1).get_raw();
const auto what = args[0].get_raw();
const auto with = args[1].get_raw();
if (what.type != game::SCRIPT_FUNCTION)
{
@ -765,26 +840,29 @@ namespace gsc
}
logfile::set_gsc_hook(what.u.codePosValue, with.u.codePosValue);
return scripting::script_value{};
});
function::add("toupper", []()
function::add("toupper", [](const function_args& args)
{
const auto string = get_argument(0).as<std::string>();
game::Scr_AddString(utils::string::to_upper(string).data());
const auto string = args[0].as<std::string>();
return utils::string::to_upper(string);
});
function::add("logprint", []()
function::add("logprint", [](const function_args& args)
{
std::string buffer{};
const auto params = game::Scr_GetNumParam();
for (auto i = 0; i < params; i++)
for (auto i = 0u; i < args.size(); ++i)
{
const auto string = game::Scr_GetString(i);
const auto string = args[i].as<std::string>();
buffer.append(string);
}
game::G_LogPrintf("%s", buffer.data());
return scripting::script_value{};
});
scripting::on_shutdown([](int free_scripts)

View File

@ -4,26 +4,44 @@
namespace gsc
{
class function_args
{
public:
function_args(std::vector<scripting::script_value>);
unsigned int size() const;
std::vector<scripting::script_value> get_raw() const;
scripting::value_wrap get(const int index) const;
scripting::value_wrap operator[](const int index) const
{
return this->get(index);
}
private:
std::vector<scripting::script_value> values_;
};
using builtin_function = void(*)();
using builtin_method = void(*)(game::scr_entref_t);
extern void* func_table[0x1000];
extern void* meth_table[0x1000];
using script_function = std::function<scripting::script_value(const function_args&)>;
using script_method = std::function<scripting::script_value(const game::scr_entref_t, const function_args&)>;
extern builtin_function func_table[0x1000];
extern builtin_method meth_table[0x1000];
game::ScriptFile* find_script(game::XAssetType /*type*/, const char* name, int /*allow_create_default*/);
void load_main_handles();
void load_init_handles();
scripting::script_value get_argument(int index);
namespace function
{
void add(const std::string& name, builtin_function function);
void add(const std::string& name, script_function function);
}
namespace method
{
void add(const std::string& name, builtin_method function);
void add(const std::string& name, script_method function);
}
}

View File

@ -23,7 +23,7 @@ namespace io
{
if (path.generic_string().find("..") != std::string::npos)
{
throw std::runtime_error("io: directory traversal is not allowed");
throw std::runtime_error("directory traversal is not allowed");
}
}
@ -38,7 +38,7 @@ namespace io
return (fs_game_path / path).generic_string();
}
throw std::runtime_error("io: fs_game is not properly defined");
throw std::runtime_error("fs_game is not properly defined");
}
}
@ -47,60 +47,59 @@ namespace io
public:
void post_unpack() override
{
gsc::function::add("fileexists", []()
gsc::function::add("fileexists", [](const gsc::function_args& args)
{
const auto path = convert_path(gsc::get_argument(0).as<std::string>());
game::Scr_AddBool(utils::io::file_exists(path));
const auto path = convert_path(args[0].as<std::string>());
return utils::io::file_exists(path);
});
gsc::function::add("writefile", []()
gsc::function::add("writefile", [](const gsc::function_args& args)
{
const auto path = convert_path(gsc::get_argument(0).as<std::string>());
const auto data = gsc::get_argument(1).as<std::string>();
const auto params = game::Scr_GetNumParam();
const auto path = convert_path(args[0].as<std::string>());
const auto data = args[1].as<std::string>();
auto append = false;
if (params > 2)
if (args.size() > 2u)
{
append = gsc::get_argument(2).as<bool>();
append = args[2].as<bool>();
}
game::Scr_AddBool(utils::io::write_file(path, data, append));
return utils::io::write_file(path, data, append);
});
gsc::function::add("readfile", []()
gsc::function::add("readfile", [](const gsc::function_args& args)
{
const auto path = convert_path(gsc::get_argument(0).as<std::string>());
game::Scr_AddString(utils::io::read_file(path).data());
const auto path = convert_path(args[0].as<std::string>());
return utils::io::read_file(path);
});
gsc::function::add("filesize", []()
gsc::function::add("filesize", [](const gsc::function_args& args)
{
const auto path = convert_path(gsc::get_argument(0).as<std::string>());
game::Scr_AddInt(static_cast<uint32_t>(utils::io::file_size(path)));
const auto path = convert_path(args[0].as<std::string>());
return static_cast<uint32_t>(utils::io::file_size(path));
});
gsc::function::add("createdirectory", []()
gsc::function::add("createdirectory", [](const gsc::function_args& args)
{
const auto path = convert_path(gsc::get_argument(0).as<std::string>());
game::Scr_AddBool(utils::io::create_directory(path));
const auto path = convert_path(args[0].as<std::string>());
return utils::io::create_directory(path);
});
gsc::function::add("directoryexists", []()
gsc::function::add("directoryexists", [](const gsc::function_args& args)
{
const auto path = convert_path(gsc::get_argument(0).as<std::string>());
game::Scr_AddBool(utils::io::directory_exists(path));
const auto path = convert_path(args[0].as<std::string>());
return utils::io::directory_exists(path);
});
gsc::function::add("directoryisempty", []()
gsc::function::add("directoryisempty", [](const gsc::function_args& args)
{
const auto path = convert_path(gsc::get_argument(0).as<std::string>());
game::Scr_AddBool(utils::io::directory_is_empty(path));
const auto path = convert_path(args[0].as<std::string>());
return utils::io::directory_is_empty(path);
});
gsc::function::add("listfiles", []()
gsc::function::add("listfiles", [](const gsc::function_args& args)
{
const auto path = convert_path(gsc::get_argument(0).as<std::string>());
const auto path = convert_path(args[0].as<std::string>());
const auto files = utils::io::list_files(path);
scripting::array array{};
@ -109,20 +108,22 @@ namespace io
array.push(file);
}
scripting::push_value(array);
return array;
});
gsc::function::add("copyfolder", []()
gsc::function::add("copyfolder", [](const gsc::function_args& args)
{
const auto source = convert_path(gsc::get_argument(0).as<std::string>());
const auto target = convert_path(gsc::get_argument(1).as<std::string>());
const auto source = convert_path(args[0].as<std::string>());
const auto target = convert_path(args[1].as<std::string>());
utils::io::copy_folder(source, target);
return scripting::script_value{};
});
gsc::function::add("removefile", []()
gsc::function::add("removefile", [](const gsc::function_args& args)
{
const auto path = convert_path(gsc::get_argument(0).as<std::string>());
game::Scr_AddBool(utils::io::remove_file(path));
const auto path = convert_path(args[0].as<std::string>());
return utils::io::remove_file(path);
});
}
};

View File

@ -15,6 +15,12 @@ namespace scripting
{
}
script_value::script_value(const value_wrap& value)
: value_(value.get_raw())
{
}
script_value::script_value(const int value)
{
game::VariableValue variable{};
@ -292,6 +298,12 @@ namespace scripting
return this->value_.get();
}
value_wrap::value_wrap(const scripting::script_value& value, int argument_index)
: value_(value)
, argument_index_(argument_index)
{
}
std::string script_value::to_string() const
{
if (this->is<int>())

View File

@ -9,12 +9,14 @@ namespace scripting
{
class entity;
class array;
class value_wrap;
class script_value
{
public:
script_value() = default;
script_value(const game::VariableValue& value);
script_value(const value_wrap& value);
script_value(int value);
script_value(unsigned int value);
@ -53,6 +55,20 @@ namespace scripting
return get<T>();
}
template <typename T, typename I = int>
T* as_ptr()
{
const auto value = this->as<I>();
if (!value)
{
throw std::runtime_error("is null");
}
return reinterpret_cast<T*>(value);
}
const game::VariableValue& get_raw() const;
variable_value value_{};
@ -62,4 +78,50 @@ namespace scripting
T get() const;
};
class value_wrap
{
public:
value_wrap(const scripting::script_value& value, int argument_index);
template <typename T>
T as() const
{
try
{
return this->value_.as<T>();
}
catch (const std::exception& e)
{
throw std::runtime_error(utils::string::va("parameter %d %s", this->argument_index_, e.what()));
}
}
template <typename T, typename I = int>
T* as_ptr()
{
try
{
return this->value_.as_ptr<T>();
}
catch (const std::exception& e)
{
throw std::runtime_error(utils::string::va("parameter %d %s", this->argument_index_, e.what()));
}
}
template <typename T>
T is() const
{
return this->value_.is<T>();
}
const game::VariableValue& get_raw() const
{
return this->value_.get_raw();
}
int argument_index_{};
scripting::script_value value_;
};
}