iw7-mod/src/client/component/gsc/script_extension.cpp
2024-01-07 22:44:05 -06:00

495 lines
12 KiB
C++

#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "component/command.hpp"
#include "component/console/console.hpp"
#include "component/dvars.hpp"
#include "component/scripting.hpp"
#include "script_error.hpp"
#include "script_extension.hpp"
#include "script_loading.hpp"
#include <utils/hook.hpp>
namespace gsc
{
std::uint16_t function_id_start = 0x326;
std::uint16_t method_id_start = 0x85DB;
constexpr size_t func_table_count = 0x1000;
constexpr size_t meth_table_count = 0x1000;
builtin_function func_table[func_table_count]{};
builtin_method meth_table[meth_table_count]{};
const game::dvar_t* developer_script = nullptr;
namespace
{
std::unordered_map<std::uint16_t, script_function> functions;
std::unordered_map<std::uint16_t, script_method> methods;
bool force_error_print = false;
std::optional<std::string> gsc_error_msg;
function_args get_arguments()
{
std::vector<scripting::script_value> args;
for (auto i = 0; static_cast<std::uint32_t>(i) < game::scr_VmPub->outparamcount; ++i)
{
const auto value = game::scr_VmPub->top[-i];
args.push_back(value);
}
return args;
}
void return_value(const scripting::script_value& value)
{
if (game::scr_VmPub->outparamcount)
{
game::Scr_ClearOutParams();
}
scripting::push_value(value);
}
std::uint16_t get_function_id()
{
const auto pos = game::scr_function_stack->pos;
return *reinterpret_cast<std::uint16_t*>(
reinterpret_cast<size_t>(pos - 2));
}
void execute_custom_function(const std::uint16_t id)
{
try
{
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& ex)
{
scr_error(ex.what());
}
}
void vm_call_builtin_function_internal(int function_id)
{
const auto custom_function_id = static_cast<std::uint16_t>(function_id); // cast for gsc-tool & our custom func map
const auto custom = functions.contains(custom_function_id);
if (custom)
{
execute_custom_function(custom_function_id);
return;
}
builtin_function func = func_table[function_id - 1]; // game does this for the stock func table
if (func == nullptr)
{
scr_error(utils::string::va("builtin function \"%s\" doesn't exist", gsc_ctx->func_name(custom_function_id).data()), true);
return;
}
func();
}
void vm_call_builtin_function_stub(utils::hook::assembler& a)
{
a.pushad64();
a.push(rcx);
a.mov(rcx, rcx); // function id is already in rcx
a.call_aligned(vm_call_builtin_function_internal);
a.pop(rcx);
a.popad64();
a.jmp(0xC0E8F9_b);
}
void execute_custom_method(const std::uint16_t id, game::scr_entref_t ent_ref)
{
try
{
const auto& method = methods[id];
const auto result = method(ent_ref, get_arguments());
const auto type = result.get_raw().type;
if (type)
{
return_value(result);
}
}
catch (const std::exception& ex)
{
scr_error(ex.what());
}
}
void vm_call_builtin_method_internal(game::scr_entref_t ent_ref, int function_id)
{
const auto custom_function_id = static_cast<std::uint16_t>(function_id); // cast for gsc-tool & our custom method map
const auto custom = methods.contains(custom_function_id);
if (custom)
{
execute_custom_method(custom_function_id, ent_ref);
return;
}
builtin_method meth = meth_table[function_id - 0x8000];
if (meth == nullptr)
{
scr_error(utils::string::va("builtin method \"%s\" doesn't exist", gsc_ctx->meth_name(custom_function_id).data()), true);
return;
}
meth(ent_ref);
}
void vm_call_builtin_method_stub(utils::hook::assembler& a)
{
a.pushad64();
a.push(rdx);
a.push(ecx);
a.mov(rdx, rdi); // function id is stored in rdi
a.mov(ecx, ebx); // ent ref is stored in ebx
a.call_aligned(vm_call_builtin_method_internal);
a.pop(rdx);
a.pop(ecx);
a.popad64();
a.jmp(0xC0E8F9_b);
}
void builtin_call_error(const std::string& error)
{
const auto function_id = get_function_id();
if (function_id > 0x1000)
{
console::warn("in call to builtin method \"%s\"%s", gsc_ctx->meth_name(function_id).data(), error.data());
}
else
{
console::warn("in call to builtin function \"%s\"%s", gsc_ctx->func_name(function_id).data(), error.data());
}
}
std::optional<std::string> get_opcode_name(const std::uint8_t opcode)
{
try
{
const auto index = gsc_ctx->opcode_enum(opcode);
return { gsc_ctx->opcode_name(index) };
}
catch (...)
{
return {};
}
}
void print_callstack()
{
for (auto frame = game::scr_VmPub->function_frame; frame != game::scr_VmPub->function_frame_start; --frame)
{
const auto pos = frame == game::scr_VmPub->function_frame ? game::scr_function_stack->pos : frame->fs.pos;
const auto function = find_function(frame->fs.pos);
if (function.has_value())
{
console::warn("\tat function \"%s\" in file \"%s.gsc\"\n", function.value().first.data(), function.value().second.data());
}
else
{
console::warn("\tat unknown location %p\n", pos);
}
}
}
void vm_error_stub(int mark_pos)
{
const bool dev_script = developer_script ? developer_script->current.enabled : false;
if (!dev_script && !force_error_print)
{
utils::hook::invoke<void>(0x510C80_b, mark_pos);
return;
}
console::warn("*********** script runtime error *************\n");
const auto opcode_id = *reinterpret_cast<std::uint8_t*>(0x6B22940_b);
const std::string error_str = gsc_error_msg.has_value()
? utils::string::va(": %s", gsc_error_msg.value().data())
: "";
if ((opcode_id >= gsc_ctx->opcode_id(xsk::gsc::opcode::OP_CallBuiltin0) && opcode_id <= gsc_ctx->opcode_id(xsk::gsc::opcode::OP_CallBuiltin))
|| (opcode_id >= gsc_ctx->opcode_id(xsk::gsc::opcode::OP_CallBuiltinMethod0) && opcode_id <= gsc_ctx->opcode_id(xsk::gsc::opcode::OP_CallBuiltinMethod)))
{
builtin_call_error(error_str);
}
else
{
const auto opcode = get_opcode_name(opcode_id);
if (opcode.has_value())
{
console::warn("while processing instruction %s%s\n", opcode.value().data(), error_str.data());
}
else
{
console::warn("while processing instruction 0x%X%s\n", opcode_id, error_str.data());
}
}
force_error_print = false;
gsc_error_msg = {};
print_callstack();
console::warn("**********************************************\n");
utils::hook::invoke<void>(0x510C80_b, mark_pos);
}
void print(const function_args& args)
{
std::string buffer{};
for (auto i = 0u; i < args.size(); ++i)
{
const auto str = args[i].to_string();
buffer.append(str);
buffer.append("\t");
}
console::info("%s\n", buffer.data());
}
scripting::script_value typeof(const function_args& args)
{
return args[0].type_name();
}
}
void scr_error(const char* error, const bool force_print)
{
force_error_print = force_print;
gsc_error_msg = error;
// TODO: check why Scr_ErrorInternal crashes the game
// is the longjmp actually happening? we're crashing at 0x140C0AC76 at a random 0xCC???
//game::Scr_ErrorInternal();
console::error("%s\n", error); // remove once we can actually get errors working
}
namespace function
{
void add(const std::string& name, script_function function)
{
if (gsc_ctx->func_exists(name))
{
const auto id = gsc_ctx->func_id(name);
functions[id] = function;
}
else
{
const auto id = ++function_id_start;
gsc_ctx->func_add(name, id);
functions[id] = function;
}
}
}
namespace method
{
void add(const std::string& name, script_method method)
{
if (gsc_ctx->meth_exists(name))
{
const auto id = gsc_ctx->meth_id(name);
methods[id] = method;
}
else
{
const auto id = ++method_id_start;
gsc_ctx->meth_add(name, static_cast<std::uint16_t>(id));
methods[id] = method;
}
}
}
function_args::function_args(std::vector<scripting::script_value> values)
: values_(values)
{
}
std::uint32_t function_args::size() const
{
return static_cast<std::uint32_t>(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 extension final : public component_interface
{
public:
void post_unpack() override
{
developer_script = game::Dvar_RegisterBool("developer_script", true, 0, "Enable developer script comments"); // enable by default for now
utils::hook::set<uint32_t>(0xBFD16B_b + 1, func_table_count); // change builtin func count
utils::hook::set<uint32_t>(0xBFD172_b + 4, static_cast<uint32_t>(reverse_b((&func_table))));
utils::hook::nop(0xC0E5CE_b, 12); // nop the call & jmp at the end of call_builtin
utils::hook::jump(0xC0E5CE_b, utils::hook::assemble(vm_call_builtin_function_stub), true);
utils::hook::inject(0xBFD5A1_b + 3, &func_table);
utils::hook::set<uint32_t>(0xBFD595_b + 2, sizeof(func_table));
utils::hook::set<uint32_t>(0xBFD182_b + 4, static_cast<uint32_t>(reverse_b((&meth_table))));
utils::hook::nop(0xC0E8EB_b, 14); // nop the lea & call at the end of call_builtin_method
utils::hook::jump(0xC0E8EB_b, utils::hook::assemble(vm_call_builtin_method_stub), true);
utils::hook::inject(0xBFD5AF_b + 3, &meth_table);
utils::hook::set<uint32_t>(0xBFD5B6_b + 2, sizeof(meth_table));
utils::hook::call(0xC0F8C1_b, vm_error_stub); // LargeLocalResetToMark
/*
if (game::environment::is_dedi())
{
function::add("isusingmatchrulesdata", [](const function_args& args)
{
// return 0 so the game doesn't override the cfg
return 0;
});
}
*/
function::add("print", [](const function_args& args)
{
print(args);
return scripting::script_value{};
});
function::add("println", [](const function_args& args)
{
print(args);
return scripting::script_value{};
});
function::add("assert", [](const function_args& args)
{
const auto expr = args[0].as<int>();
if (!expr)
{
throw std::runtime_error("assert fail");
}
return scripting::script_value{};
});
function::add("assertex", [](const function_args& args)
{
const auto expr = args[0].as<int>();
if (!expr)
{
const auto error = args[1].as<std::string>();
throw std::runtime_error(error);
}
return scripting::script_value{};
});
function::add("getfunction", [](const function_args& args)
{
const auto filename = args[0].as<std::string>();
const auto function = args[1].as<std::string>();
if (!scripting::script_function_table[filename].contains(function))
{
throw std::runtime_error("function not found");
}
return scripting::function{scripting::script_function_table[filename][function]};
});
/*
function::add("replacefunc", [](const function_args& args)
{
const auto what = args[0].get_raw();
const auto with = args[1].get_raw();
if (what.type != game::VAR_FUNCTION || with.type != game::VAR_FUNCTION)
{
throw std::runtime_error("replacefunc: parameter 1 must be a function");
}
logfile::set_gsc_hook(what.u.codePosValue, with.u.codePosValue);
return scripting::script_value{};
});
*/
function::add("toupper", [](const function_args& args)
{
const auto string = args[0].as<std::string>();
return utils::string::to_upper(string);
});
/*
function::add("logprint", [](const function_args& args)
{
std::string buffer{};
for (auto i = 0u; i < args.size(); ++i)
{
const auto string = args[i].as<std::string>();
buffer.append(string);
}
game::G_LogPrintf("%s", buffer.data());
return scripting::script_value{};
});
*/
function::add("executecommand", [](const function_args& args)
{
command::execute(args[0].as<std::string>(), false);
return scripting::script_value{};
});
function::add("typeof", typeof);
function::add("type", typeof);
method::add("test_method", [](game::scr_entref_t ent_ref, const function_args& args)
{
print(args);
return scripting::script_value{};
});
}
};
}
REGISTER_COMPONENT(gsc::extension)